mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-11 02:57:39 +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/CloudPluginService.hpp
|
||||||
plugin/PluginFsUtils.cpp
|
plugin/PluginFsUtils.cpp
|
||||||
plugin/PluginFsUtils.hpp
|
plugin/PluginFsUtils.hpp
|
||||||
plugin/PluginCatalog.cpp
|
|
||||||
plugin/PluginCatalog.hpp
|
|
||||||
plugin/PluginLoader.cpp
|
plugin/PluginLoader.cpp
|
||||||
plugin/PluginLoader.hpp
|
plugin/PluginLoader.hpp
|
||||||
plugin/PluginDescriptor.hpp
|
plugin/PluginDescriptor.hpp
|
||||||
|
|||||||
@@ -1121,7 +1121,7 @@ void GUI_App::shutdown()
|
|||||||
if (m_is_recreating_gui) return;
|
if (m_is_recreating_gui) return;
|
||||||
stop_http_server();
|
stop_http_server();
|
||||||
set_closing(true);
|
set_closing(true);
|
||||||
Slic3r::PluginManager::instance().get_loader().set_shutting_down();
|
Slic3r::PluginManager::instance().set_shutting_down();
|
||||||
|
|
||||||
if (m_agent)
|
if (m_agent)
|
||||||
m_agent->set_printer_agent(nullptr);
|
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.subscribe_on_unload_callback(PluginHostUi::close_windows_for_plugin);
|
||||||
plugin_mgr.get_loader().subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
plugin_mgr.subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
||||||
plugin_mgr.get_loader().subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
|
plugin_mgr.subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
||||||
plugin_mgr.get_loader().subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
|
plugin_mgr.subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
|
||||||
plugin_mgr.get_loader().subscribe_on_capability_load_callback(
|
plugin_mgr.subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
|
||||||
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
|
plugin_mgr.subscribe_on_capability_load_callback(
|
||||||
|
[refresh_plugins_dialog](const PluginCapabilityId& capability) {
|
||||||
if (capability.type == PluginCapabilityType::PrinterConnection)
|
if (capability.type == PluginCapabilityType::PrinterConnection)
|
||||||
NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name);
|
NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name);
|
||||||
refresh_plugins_dialog();
|
refresh_plugins_dialog();
|
||||||
@@ -2740,8 +2741,8 @@ void GUI_App::init_plugin_gui_wiring()
|
|||||||
plater->revalidate_current_plate_if_plugins_missing();
|
plater->revalidate_current_plate_if_plugins_missing();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
plugin_mgr.get_loader().subscribe_on_capability_unload_callback(
|
plugin_mgr.subscribe_on_capability_unload_callback(
|
||||||
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
|
[refresh_plugins_dialog](const PluginCapabilityId& capability) {
|
||||||
if (capability.type == PluginCapabilityType::PrinterConnection)
|
if (capability.type == PluginCapabilityType::PrinterConnection)
|
||||||
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
|
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
|
||||||
refresh_plugins_dialog();
|
refresh_plugins_dialog();
|
||||||
@@ -3162,17 +3163,16 @@ bool GUI_App::on_init_inner()
|
|||||||
// plugins are discovered even before the network agent is ready.
|
// plugins are discovered even before the network agent is ready.
|
||||||
const std::string preset_folder = app_config->get("preset_folder");
|
const std::string preset_folder = app_config->get("preset_folder");
|
||||||
if (!preset_folder.empty()) {
|
if (!preset_folder.empty()) {
|
||||||
plugin_mgr.get_catalog().set_cloud_plugin_dir(preset_folder);
|
plugin_mgr.set_cloud_user(preset_folder);
|
||||||
plugin_mgr.get_loader().set_cloud_user_id(preset_folder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin_mgr.discover_plugins(false, true);
|
plugin_mgr.discover_plugins(false, true);
|
||||||
|
|
||||||
init_plugin_gui_wiring();
|
init_plugin_gui_wiring();
|
||||||
|
|
||||||
for (const std::string& plugin_key : plugin_mgr.get_catalog().get_enabled_plugin_keys()) {
|
for (const std::string& plugin_key : plugin_mgr.get_enabled_plugin_keys()) {
|
||||||
if (!plugin_mgr.get_loader().is_plugin_loaded(plugin_key)) {
|
if (!plugin_mgr.is_plugin_loaded(plugin_key)) {
|
||||||
plugin_mgr.get_loader().load_plugin(plugin_mgr.get_catalog(), plugin_key, false);
|
plugin_mgr.load_plugin(plugin_key, false);
|
||||||
BOOST_LOG_TRIVIAL(info) << "Auto-loading plugin on startup: " << plugin_key;
|
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()) {
|
if (m_agent && m_agent->is_user_login()) {
|
||||||
enable_user_preset_folder(true);
|
enable_user_preset_folder(true);
|
||||||
plugin_mgr.get_catalog().set_cloud_plugin_dir(m_agent->get_user_id());
|
plugin_mgr.set_cloud_user(m_agent->get_user_id());
|
||||||
plugin_mgr.get_loader().set_cloud_user_id(m_agent->get_user_id());
|
|
||||||
// If there is a user logged in we do an immediate sync.
|
// If there is a user logged in we do an immediate sync.
|
||||||
std::vector<std::string> not_found, unauthorized;
|
std::vector<std::string> not_found, unauthorized;
|
||||||
plugin_mgr.fetch_plugins_from_cloud(¬_found, &unauthorized);
|
plugin_mgr.fetch_plugins_from_cloud(¬_found, &unauthorized);
|
||||||
@@ -3203,8 +3202,7 @@ bool GUI_App::on_init_inner()
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enable_user_preset_folder(false);
|
enable_user_preset_folder(false);
|
||||||
plugin_mgr.get_catalog().set_cloud_plugin_dir("");
|
plugin_mgr.set_cloud_user("");
|
||||||
plugin_mgr.get_loader().set_cloud_user_id("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BBS if load user preset failed
|
// 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();
|
remove_user_presets();
|
||||||
enable_user_preset_folder(false);
|
enable_user_preset_folder(false);
|
||||||
Slic3r::PluginManager::instance().get_loader().unload_cloud_plugins();
|
Slic3r::PluginManager::instance().unload_cloud_plugins();
|
||||||
Slic3r::PluginManager::instance().get_catalog().clear_cloud_plugin_catalog();
|
Slic3r::PluginManager::instance().clear_cloud_plugin_catalog();
|
||||||
Slic3r::PluginManager::instance().get_catalog().set_cloud_plugin_dir("");
|
Slic3r::PluginManager::instance().set_cloud_user("");
|
||||||
Slic3r::PluginManager::instance().get_loader().set_cloud_user_id("");
|
|
||||||
preset_bundle->load_user_presets(DEFAULT_USER_FOLDER_NAME, ForwardCompatibilitySubstitutionRule::Enable);
|
preset_bundle->load_user_presets(DEFAULT_USER_FOLDER_NAME, ForwardCompatibilitySubstitutionRule::Enable);
|
||||||
mainframe->update_side_preset_ui();
|
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();
|
std::string user_id = m_agent->get_user_id();
|
||||||
app_config->set("preset_folder", user_id);
|
app_config->set("preset_folder", user_id);
|
||||||
GUI::wxGetApp().preset_bundle->update_user_presets_directory(user_id);
|
GUI::wxGetApp().preset_bundle->update_user_presets_directory(user_id);
|
||||||
PluginManager::instance().get_catalog().set_cloud_plugin_dir(user_id);
|
PluginManager::instance().set_cloud_user(user_id);
|
||||||
PluginManager::instance().get_loader().set_cloud_user_id(user_id);
|
|
||||||
} else {
|
} else {
|
||||||
BOOST_LOG_TRIVIAL(info) << "preset_folder: set to empty";
|
BOOST_LOG_TRIVIAL(info) << "preset_folder: set to empty";
|
||||||
app_config->set("preset_folder", "");
|
app_config->set("preset_folder", "");
|
||||||
GUI::wxGetApp().preset_bundle->update_user_presets_directory(DEFAULT_USER_FOLDER_NAME);
|
GUI::wxGetApp().preset_bundle->update_user_presets_directory(DEFAULT_USER_FOLDER_NAME);
|
||||||
PluginManager::instance().get_catalog().set_cloud_plugin_dir("");
|
PluginManager::instance().set_cloud_user("");
|
||||||
PluginManager::instance().get_loader().set_cloud_user_id("");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8230,7 +8225,7 @@ void GUI_App::open_plugins_dialog(size_t open_on_tab, const std::string& highlig
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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->set_open_terminal_dlg_fn();
|
||||||
m_plugins_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& event) {
|
m_plugins_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& event) {
|
||||||
if (event.GetEventObject() == m_plugins_dlg)
|
if (event.GetEventObject() == m_plugins_dlg)
|
||||||
|
|||||||
@@ -683,8 +683,13 @@ std::string OptionsGroup::pick_plugin(const ConfigOptionDef& opt)
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto caps = manager.get_loader().get_plugin_capabilities_by_type(plugin_type);
|
struct CapabilityRef { std::string plugin_key; std::string name; };
|
||||||
caps.erase(std::remove_if(caps.begin(), caps.end(), [](const auto& cap) { return !cap || !cap->enabled; }), caps.end());
|
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()) {
|
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);
|
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());
|
entries.reserve(caps.size());
|
||||||
for (const auto& cap : caps) {
|
for (const auto& cap : caps) {
|
||||||
Slic3r::PluginDescriptor descriptor;
|
Slic3r::PluginDescriptor descriptor;
|
||||||
wxString package_name = manager.get_catalog().try_get_plugin_descriptor(cap->plugin_key, descriptor)
|
wxString package_name = manager.try_get_plugin_descriptor(cap.plugin_key, descriptor)
|
||||||
? from_u8(descriptor.name) : from_u8(cap->plugin_key);
|
? from_u8(descriptor.name) : from_u8(cap.plugin_key);
|
||||||
entries.push_back({
|
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
|
// 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).
|
// when the preset is serialized (see ConfigBase::save_plugin_collection).
|
||||||
Slic3r::PluginDescriptor descriptor;
|
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 {};
|
return {};
|
||||||
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Picked plugin capability: " << selection.name;
|
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).
|
// Returns the {plugin_key, name} of the selected capability (capability path).
|
||||||
CapabilityEntry selected_capability() const;
|
CapabilityEntry selected_capability() const;
|
||||||
|
|
||||||
void update_plugin_list(const std::vector<PluginDescriptor>& updated_plugins) { m_plugins = updated_plugins; }
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void build_ui(const wxString& plugin_type_label);
|
void build_ui(const wxString& plugin_type_label);
|
||||||
void build_capability_ui(const wxString& plugin_type_label);
|
void build_capability_ui(const wxString& plugin_type_label);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <stdexcept>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <wx/dialog.h>
|
#include <wx/dialog.h>
|
||||||
@@ -123,19 +124,19 @@ struct PluginDialogItem
|
|||||||
constexpr bool kFetchCloudCatalog = true;
|
constexpr bool kFetchCloudCatalog = true;
|
||||||
constexpr bool kUseCurrentCloudCatalog = false;
|
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()
|
std::vector<PluginDescriptor> current_cloud_catalog_snapshot()
|
||||||
{
|
{
|
||||||
const auto& catalog = PluginManager::instance().get_catalog();
|
|
||||||
|
|
||||||
std::vector<PluginDescriptor> cloud_entries;
|
std::vector<PluginDescriptor> cloud_entries;
|
||||||
auto append_cloud_entries = [&cloud_entries](const std::vector<PluginDescriptor>& entries) {
|
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
|
||||||
for (const PluginDescriptor& entry : entries)
|
if (entry.is_cloud_plugin())
|
||||||
if (entry.is_cloud_plugin())
|
cloud_entries.push_back(entry);
|
||||||
cloud_entries.push_back(entry);
|
|
||||||
};
|
|
||||||
|
|
||||||
append_cloud_entries(catalog.get_all_plugin_descriptors());
|
|
||||||
append_cloud_entries(catalog.get_invalid_plugins());
|
|
||||||
return cloud_entries;
|
return cloud_entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,26 +166,30 @@ void refresh_plugin_catalog_blocking(bool fetch_cloud)
|
|||||||
manager.rescan_plugins();
|
manager.rescan_plugins();
|
||||||
|
|
||||||
if (!fetch_cloud) {
|
if (!fetch_cloud) {
|
||||||
manager.get_catalog().update_cloud_catalog(current_cloud_catalog);
|
manager.update_cloud_catalog(current_cloud_catalog);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.fetch_plugins_from_cloud(¬_found, &unauthorized);
|
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)
|
||||||
for (const auto& uuid : not_found) {
|
plater->get_notification_manager()->push_notification(
|
||||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
NotificationType::CustomNotification,
|
||||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||||
format(_L("Plugin %s is no longer available."), uuid));
|
format(_L("Plugin %s is no longer available."), uuid));
|
||||||
}
|
for (const auto& uuid : unauthorized)
|
||||||
for (const auto& uuid : unauthorized) {
|
plater->get_notification_manager()->push_notification(
|
||||||
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
|
NotificationType::CustomNotification,
|
||||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||||
format(_L("Plugin %s access is unauthorized."), uuid));
|
format(_L("Plugin %s access is unauthorized."), uuid));
|
||||||
}
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string to_string(PluginUpdateStatus status);
|
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 build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
||||||
{
|
{
|
||||||
PluginDialogItem item;
|
PluginDialogItem item;
|
||||||
const auto& loader = PluginManager::instance().get_loader();
|
PluginManager& manager = PluginManager::instance();
|
||||||
|
|
||||||
item.plugin_key = descriptor.plugin_key;
|
item.plugin_key = descriptor.plugin_key;
|
||||||
item.display_name = descriptor.name;
|
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
|
// 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.
|
// 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.sort_version = item.installed_version.empty() ? item.latest_version : item.installed_version;
|
||||||
item.type_label = descriptor.type_label();
|
// A package that is not loaded has no capabilities, so its row contributes none and does not
|
||||||
item.type_key = plugin_capability_type_to_string(descriptor.primary_capability_type());
|
// 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
|
// "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
|
// 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()) {
|
if (descriptor.is_cloud_plugin() && !descriptor.display_types.empty()) {
|
||||||
for (const std::string& label : descriptor.display_types)
|
for (const std::string& label : descriptor.display_types)
|
||||||
if (std::find(item.type_labels.begin(), item.type_labels.end(), label) == item.type_labels.end())
|
if (std::find(item.type_labels.begin(), item.type_labels.end(), label) == item.type_labels.end())
|
||||||
item.type_labels.push_back(label);
|
item.type_labels.push_back(label);
|
||||||
} else {
|
} else {
|
||||||
for (PluginCapabilityType type : descriptor.capability_types) {
|
for (const auto& capability : capabilities) {
|
||||||
const std::string label = plugin_capability_type_display_name(type);
|
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())
|
if (std::find(item.type_labels.begin(), item.type_labels.end(), label) == item.type_labels.end())
|
||||||
item.type_labels.push_back(label);
|
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.is_cloud_plugin = descriptor.is_cloud_plugin();
|
||||||
item.has_local_package = descriptor.has_local_package();
|
item.has_local_package = descriptor.has_local_package();
|
||||||
item.unauthorized = descriptor.is_unauthorized();
|
item.unauthorized = descriptor.is_unauthorized();
|
||||||
item.has_script_capability = descriptor.has_capability_type(Slic3r::PluginCapabilityType::Script);
|
item.is_loaded = manager.is_plugin_loaded(descriptor.plugin_key);
|
||||||
item.is_loaded = loader.is_plugin_loaded(descriptor.plugin_key);
|
item.loading = manager.is_plugin_load_in_progress(descriptor.plugin_key);
|
||||||
item.loading = loader.is_plugin_load_in_progress(descriptor.plugin_key);
|
item.has_script_capability = false;
|
||||||
if (item.is_loaded) {
|
for (const auto& capability : capabilities) {
|
||||||
for (const auto& cap : loader.get_loaded_plugin_capabilities(descriptor.plugin_key)) {
|
item.capabilities.push_back({capability->name(), plugin_capability_type_display_name(capability->type()),
|
||||||
if (cap) {
|
plugin_capability_type_to_string(capability->type()), capability->is_enabled(), true, false});
|
||||||
item.capabilities.push_back({cap->name, plugin_capability_type_display_name(cap->type),
|
if (capability->type() == PluginCapabilityType::Script)
|
||||||
plugin_capability_type_to_string(cap->type), cap->enabled, true, false});
|
item.has_script_capability = true;
|
||||||
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.sharing_token = descriptor.sharing_token;
|
item.sharing_token = descriptor.sharing_token;
|
||||||
item.thumbnail_url = descriptor.thumbnail_url;
|
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)
|
: WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style)
|
||||||
{ create_webview("web/dialog/PluginsDialog/index.html", _L("Plugins"), wxSize(900, 820), wxSize(760, 715)); }
|
{ 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()
|
void PluginsDialog::set_open_terminal_dlg_fn()
|
||||||
{
|
{
|
||||||
@@ -467,8 +463,8 @@ void PluginsDialog::resolve_pending_activation()
|
|||||||
if (m_activating_plugin_key.empty())
|
if (m_activating_plugin_key.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
PluginManager& manager = PluginManager::instance();
|
||||||
if (loader.is_plugin_load_in_progress(m_activating_plugin_key))
|
if (manager.is_plugin_load_in_progress(m_activating_plugin_key))
|
||||||
return; // Still loading: keep the "Activating..." message until the load resolves.
|
return; // Still loading: keep the "Activating..." message until the load resolves.
|
||||||
|
|
||||||
const std::string plugin_key = m_activating_plugin_key;
|
const std::string plugin_key = m_activating_plugin_key;
|
||||||
@@ -478,7 +474,7 @@ void PluginsDialog::resolve_pending_activation()
|
|||||||
PluginDescriptor descriptor;
|
PluginDescriptor descriptor;
|
||||||
if (get_descriptor(plugin_key, descriptor) && descriptor.has_error())
|
if (get_descriptor(plugin_key, descriptor) && descriptor.has_error())
|
||||||
show_status(wxString::Format(_L("Failed to activate \"%s\"."), plugin_display_name(plugin_key)), "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");
|
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.
|
// 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["sort_order"] = to_string(m_plugin_sort_order);
|
||||||
response["data"] = nlohmann::json::array();
|
response["data"] = nlohmann::json::array();
|
||||||
|
|
||||||
const auto& catalog = PluginManager::instance().get_catalog();
|
const auto rows = PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true);
|
||||||
const auto valid = catalog.get_all_plugin_descriptors();
|
BOOST_LOG_TRIVIAL(info) << "Prepared " << rows.size() << " plugin rows for Plugins dialog";
|
||||||
const auto invalid = catalog.get_invalid_plugins();
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Prepared " << valid.size() + invalid.size() << " plugin rows for Plugins dialog";
|
|
||||||
|
|
||||||
std::vector<PluginDialogItem> items;
|
std::vector<PluginDialogItem> items;
|
||||||
items.reserve(valid.size() + invalid.size());
|
items.reserve(rows.size());
|
||||||
|
|
||||||
for (const PluginDescriptor& row : valid)
|
for (const PluginDescriptor& row : rows)
|
||||||
items.push_back(build_plugin_dialog_item(row));
|
|
||||||
|
|
||||||
for (const PluginDescriptor& row : invalid)
|
|
||||||
items.push_back(build_plugin_dialog_item(row));
|
items.push_back(build_plugin_dialog_item(row));
|
||||||
|
|
||||||
// In-place sort
|
// 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
|
bool PluginsDialog::get_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const
|
||||||
{
|
{
|
||||||
const auto& catalog = PluginManager::instance().get_catalog();
|
PluginManager& manager = PluginManager::instance();
|
||||||
if (const PluginDescriptor* valid_descriptor = catalog.find_valid_plugin_descriptor(plugin_key)) {
|
if (manager.try_get_valid_plugin_descriptor(plugin_key, descriptor))
|
||||||
descriptor = *valid_descriptor;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
return manager.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_invalid_package();
|
||||||
return catalog.try_get_invalid_plugin_descriptor(plugin_key, descriptor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<LoadedPluginCapability> PluginsDialog::get_capability(const std::string& plugin_key,
|
std::shared_ptr<PluginCapabilityInterface> PluginsDialog::get_capability(const std::string& plugin_key,
|
||||||
PluginCapabilityType type,
|
PluginCapabilityType type,
|
||||||
const std::string& capability_name) const
|
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)
|
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();
|
PluginManager& manager = PluginManager::instance();
|
||||||
if (!enabled) {
|
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;
|
BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin from Plugins dialog: " << plugin_key;
|
||||||
show_status(_L("Failed to unload plugin."), "warn");
|
show_status(_L("Failed to unload plugin."), "warn");
|
||||||
send_plugins();
|
send_plugins();
|
||||||
@@ -632,9 +623,6 @@ void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
|
|||||||
return;
|
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 (!available_actions.can_toggle) {
|
||||||
if (dialog_item.unauthorized && available_actions.toggle_installs_cloud_plugin == false && row_data.has_local_package() == false) {
|
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.";
|
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();
|
send_plugins();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PluginDescriptor* descriptor_ptr = manager.get_catalog().find_valid_plugin_descriptor(plugin_key);
|
PluginDescriptor valid_descriptor;
|
||||||
if (!descriptor_ptr) {
|
if (!manager.try_get_valid_plugin_descriptor(plugin_key, valid_descriptor)) {
|
||||||
show_status(_L("Plugin manifest was not found."), "warn");
|
show_status(_L("Plugin manifest was not found."), "warn");
|
||||||
send_plugins();
|
send_plugins();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manager.get_loader().is_plugin_load_in_progress(plugin_key)) {
|
if (manager.is_plugin_load_in_progress(plugin_key)) {
|
||||||
send_plugins();
|
send_plugins();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Starting plugin load from web dialog: " << plugin_key;
|
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();
|
send_plugins();
|
||||||
// Loading is asynchronous: show a pending message now; resolve_pending_activation() turns it into
|
// 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().
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
PluginManager& manager = PluginManager::instance();
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Enabling plugin capability: " << plugin_key << " | " << capability_name;
|
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 {
|
} else {
|
||||||
// check if the capability is currently in use here.
|
// check if the capability is currently in use here.
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Disabling plugin capability: " << plugin_key << " | " << capability_name;
|
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();
|
send_plugins();
|
||||||
@@ -795,8 +783,7 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!PluginManager::instance().get_loader().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation,
|
if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error))
|
||||||
error))
|
|
||||||
return report_inspection_failure();
|
return report_inspection_failure();
|
||||||
} catch (const std::exception& ex) {
|
} catch (const std::exception& ex) {
|
||||||
error = ex.what();
|
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)
|
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();
|
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
|
std::string error;
|
||||||
// loop; the WebView could re-dispatch this command mid-run. Refuse the overlapping run.
|
ExecutionResult result;
|
||||||
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;
|
|
||||||
|
|
||||||
auto complete_with_error = [this, &manager, &plugin_key](const std::string& plugin_error, const wxString& status_message) {
|
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;
|
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;
|
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;
|
BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin after script error. plugin_key=" << plugin_key;
|
||||||
|
|
||||||
send_plugins();
|
send_plugins();
|
||||||
@@ -948,74 +907,12 @@ void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::
|
|||||||
show_status(message, "error");
|
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;
|
wxBusyCursor busy;
|
||||||
try {
|
result = manager.run_script_capability(plugin_key, capability_name, error);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!error.empty()) {
|
if (!error.empty()) {
|
||||||
plugin.reset();
|
|
||||||
cap.reset();
|
|
||||||
complete_with_error(error, wxString());
|
complete_with_error(error, wxString());
|
||||||
return;
|
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
|
BOOST_LOG_TRIVIAL(info) << "Script plugin execution completed. plugin_key=" << plugin_key
|
||||||
<< " status=" << static_cast<int>(result.status) << " message=" << result.message << " data=" << result.data;
|
<< " 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) {
|
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());
|
complete_with_error(result.message, wxString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1189,18 +1083,35 @@ void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
|
|||||||
if (plugin_key.empty())
|
if (plugin_key.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PluginManager& manager = PluginManager::instance();
|
const bool was_loaded = PluginManager::instance().is_plugin_loaded(plugin_key);
|
||||||
const bool was_loaded = manager.get_loader().is_plugin_loaded(plugin_key);
|
std::pair<bool, std::string> reload_result{false, ""};
|
||||||
if (!manager.get_loader().unload_plugin(plugin_key)) {
|
try {
|
||||||
show_status(_L("Failed to unload plugin."), "warn");
|
reload_result = run_with_dialog_wait(
|
||||||
send_plugins();
|
[plugin_key, was_loaded]() -> std::pair<bool, std::string> {
|
||||||
return;
|
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 (!reload_result.first) {
|
||||||
|
show_status(reload_result.second.empty() ? _L("Failed to reload plugin.") : from_u8(reload_result.second), "warn");
|
||||||
if (!was_loaded && !manager.get_loader().unload_plugin(plugin_key)) {
|
|
||||||
show_status(_L("Plugin reloaded, but failed to restore the inactive state."), "warn");
|
|
||||||
send_plugins();
|
send_plugins();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1216,7 +1127,7 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
PluginManager& manager = PluginManager::instance();
|
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;
|
std::string error;
|
||||||
if (plugin.has_local_package()) {
|
if (plugin.has_local_package()) {
|
||||||
@@ -1226,7 +1137,7 @@ void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
|
|||||||
return;
|
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))) {
|
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) {
|
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();
|
send_plugins();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include "PluginSort.hpp"
|
#include "PluginSort.hpp"
|
||||||
#include "slic3r/plugin/PluginDescriptor.hpp"
|
#include "slic3r/plugin/PluginDescriptor.hpp"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -17,6 +19,7 @@
|
|||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <wx/evtloop.h>
|
#include <wx/evtloop.h>
|
||||||
|
#include <wx/app.h>
|
||||||
#include <wx/progdlg.h>
|
#include <wx/progdlg.h>
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
#include <wx/timer.h>
|
#include <wx/timer.h>
|
||||||
@@ -25,7 +28,7 @@ class wxTimer;
|
|||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
struct LoadedPluginCapability;
|
class PluginCapabilityInterface;
|
||||||
enum class PluginCapabilityType;
|
enum class PluginCapabilityType;
|
||||||
|
|
||||||
namespace GUI {
|
namespace GUI {
|
||||||
@@ -55,7 +58,7 @@ private:
|
|||||||
nlohmann::json build_plugins_payload() const;
|
nlohmann::json build_plugins_payload() const;
|
||||||
|
|
||||||
bool get_descriptor(const std::string& plugin_key, Slic3r::PluginDescriptor& descriptor) 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_plugin_catalog_async(const wxString& title, const wxString& message, bool fetch_cloud);
|
||||||
void refresh_plugins();
|
void refresh_plugins();
|
||||||
@@ -91,34 +94,51 @@ private:
|
|||||||
const wxString& title,
|
const wxString& title,
|
||||||
const wxString& message,
|
const wxString& message,
|
||||||
int maximum = 100,
|
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);
|
wxProgressDialog* progress = new wxProgressDialog(title, message, maximum, this, style);
|
||||||
wxTimer* timer = new wxTimer();
|
wxTimer* timer = new wxTimer();
|
||||||
|
|
||||||
timer->Bind(wxEVT_TIMER, [progress, message](wxTimerEvent&) {
|
timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) {
|
||||||
if (progress)
|
if (alive->load(std::memory_order_acquire) && progress)
|
||||||
progress->Pulse(message);
|
progress->Pulse(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
timer->Start(100);
|
timer->Start(100);
|
||||||
|
|
||||||
std::thread([this,
|
std::thread([alive,
|
||||||
progress,
|
progress,
|
||||||
timer,
|
timer,
|
||||||
run = std::forward<Run>(run),
|
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 {
|
try {
|
||||||
run();
|
run();
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what();
|
||||||
} catch (...) {
|
} 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();
|
timer->Stop();
|
||||||
delete timer;
|
delete timer;
|
||||||
|
|
||||||
progress->Destroy();
|
if (alive->load(std::memory_order_acquire)) {
|
||||||
on_finish();
|
progress->Destroy();
|
||||||
|
on_finish();
|
||||||
|
} else if (finish_after_dialog_destroyed) {
|
||||||
|
on_finish();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}).detach();
|
}).detach();
|
||||||
}
|
}
|
||||||
@@ -157,7 +177,7 @@ private:
|
|||||||
state->exception = std::current_exception();
|
state->exception = std::current_exception();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
on_finish, title, message, maximum, style);
|
on_finish, title, message, maximum, style, true);
|
||||||
|
|
||||||
if (!finished)
|
if (!finished)
|
||||||
loop.Run();
|
loop.Run();
|
||||||
@@ -190,7 +210,7 @@ private:
|
|||||||
state->exception = std::current_exception();
|
state->exception = std::current_exception();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
on_finish, title, message, maximum, style);
|
on_finish, title, message, maximum, style, true);
|
||||||
|
|
||||||
if (!finished)
|
if (!finished)
|
||||||
loop.Run();
|
loop.Run();
|
||||||
@@ -210,6 +230,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::function<void()> m_open_terminal_dlg_fn;
|
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;
|
PluginSortKey m_plugin_sort_key = PluginSortKey::None;
|
||||||
PluginSortOrder m_plugin_sort_order = PluginSortOrder::Asc;
|
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
|
// 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.
|
// 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;
|
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;
|
ExecutionResult exec_result;
|
||||||
try {
|
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)
|
void NetworkAgentFactory::register_python_plugin(const std::string& plugin_key)
|
||||||
{
|
{
|
||||||
auto capabilities = PluginManager::instance().get_loader().get_plugin_capabilities_by_type(
|
for (const auto& capability : PluginManager::instance().get_plugin_capabilities(plugin_key, PluginCapabilityType::PrinterConnection,
|
||||||
plugin_key, PluginCapabilityType::PrinterConnection);
|
/*only_enabled=*/true))
|
||||||
for (const auto& capability : capabilities)
|
register_python_printer_agent(plugin_key, capability->name());
|
||||||
if (capability && capability->enabled)
|
|
||||||
register_python_printer_agent(plugin_key, capability->name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkAgentFactory::register_python_printer_agent(const std::string& plugin_key, const std::string& capability_name)
|
void NetworkAgentFactory::register_python_printer_agent(const std::string& plugin_key, const std::string& capability_name)
|
||||||
{
|
{
|
||||||
PluginManager& plugin_manager = PluginManager::instance();
|
PluginManager& plugin_manager = PluginManager::instance();
|
||||||
|
|
||||||
auto cap = plugin_manager.get_loader().get_plugin_capability_by_name(plugin_key, PluginCapabilityType::PrinterConnection,
|
auto cap = plugin_manager.get_plugin_capability(plugin_key, capability_name, PluginCapabilityType::PrinterConnection,
|
||||||
capability_name);
|
/*only_enabled=*/false);
|
||||||
if (!cap) {
|
if (!cap) {
|
||||||
BOOST_LOG_TRIVIAL(warning) << "Printer-agent capability '" << capability_name << "' not found for plugin '" << plugin_key << "'";
|
BOOST_LOG_TRIVIAL(warning) << "Printer-agent capability '" << capability_name << "' not found for plugin '" << plugin_key << "'";
|
||||||
return;
|
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 << "'";
|
BOOST_LOG_TRIVIAL(warning) << "Printer-agent capability '" << capability_name << "' is disabled for plugin '" << plugin_key << "'";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginDescriptor descriptor;
|
auto plugin = std::dynamic_pointer_cast<PrinterAgentPluginCapability>(cap);
|
||||||
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);
|
|
||||||
|
|
||||||
if (!plugin) {
|
if (!plugin) {
|
||||||
BOOST_LOG_TRIVIAL(warning) << "Loaded plugin capability '" << capability_name << "' is not a printer-agent 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.
|
// 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.
|
// 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,
|
// only_enabled defaults to true here, so a capability that changed identity (reload) or was
|
||||||
capability_name);
|
// merely disabled both surface the same way: current_cap no longer equals cap.
|
||||||
if (current_cap != cap || !current_cap->enabled) {
|
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
|
BOOST_LOG_TRIVIAL(debug) << "Printer-agent capability '" << capability_name << "' from plugin '" << plugin_key
|
||||||
<< "' changed or was disabled during registration";
|
<< "' changed or was disabled during registration";
|
||||||
return;
|
return;
|
||||||
@@ -322,7 +321,9 @@ void NetworkAgentFactory::register_python_printer_agent(const std::string& plugi
|
|||||||
[&plugin_key, &capability_name](const auto& entry) {
|
[&plugin_key, &capability_name](const auto& entry) {
|
||||||
return entry.first.first == plugin_key && entry.first.second == capability_name;
|
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
|
BOOST_LOG_TRIVIAL(debug) << "Printer-agent capability '" << capability_name << "' from plugin '" << plugin_key
|
||||||
<< "' was disabled before registry commit";
|
<< "' was disabled before registry commit";
|
||||||
return;
|
return;
|
||||||
@@ -448,6 +449,18 @@ void NetworkAgentFactory::deregister_python_printer_agent(const std::string& plu
|
|||||||
if (cached_agent)
|
if (cached_agent)
|
||||||
cached_agent->disconnect_printer();
|
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 '"
|
BOOST_LOG_TRIVIAL(info) << "Deregistered Python printer-agent capability '" << capability_name << "' from plugin '"
|
||||||
<< plugin_key << "' with agent ID '" << agent_id << "'";
|
<< plugin_key << "' with agent ID '" << agent_id << "'";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "libslic3r/Utils.hpp"
|
#include "libslic3r/Utils.hpp"
|
||||||
|
|
||||||
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
#include <cstdlib>
|
#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_it = canon_root.begin();
|
||||||
auto root_end = canon_root.end();
|
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
|
// 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;
|
++root_it;
|
||||||
++cand_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 version; // Selected plugin version
|
||||||
std::string latest_version; // Latest available cloud version fallback when changelog is unavailable.
|
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::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::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 plugin_root; // Installed plugin directory, even when entry_path is invalid or ambiguous.
|
||||||
std::string entry_path; // Full path to the installed plugin entry file
|
std::string entry_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::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.
|
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.
|
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 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.
|
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 has_local_package() const { return !is_cloud_plugin() || cloud->installed || !plugin_root.empty() || !entry_path.empty(); }
|
||||||
bool is_metadata_valid() const { return metadata_valid; }
|
bool is_metadata_valid() const { return metadata_valid; }
|
||||||
|
|
||||||
// Capability-type helpers. A package may materialize several capability types;
|
// A package that exists on disk but whose manifest could not be parsed.
|
||||||
// these accessors give callers that still reason about a single "type" (catalog
|
//
|
||||||
// display, cloud overlay, dispatch — finalized in later tasks) a stable view.
|
// Deliberately not just !is_metadata_valid(): a cloud plugin that is merely available
|
||||||
bool has_capability_type(PluginCapabilityType t) const
|
// (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
|
||||||
return std::find(capability_types.begin(), capability_types.end(), t) != capability_types.end();
|
// whether there is a local package behind the descriptor at all.
|
||||||
}
|
bool is_invalid_package() const { return !metadata_valid && has_local_package(); }
|
||||||
PluginCapabilityType primary_capability_type() const
|
|
||||||
{
|
|
||||||
return capability_types.empty() ? PluginCapabilityType::Unknown : capability_types.front();
|
|
||||||
}
|
|
||||||
// Set the package to a single capability type (metadata/cloud sources currently
|
|
||||||
// declare one type; multi-type discovery is finalized in later tasks).
|
|
||||||
void set_capability_type(PluginCapabilityType t) { capability_types.assign(1, t); }
|
|
||||||
// Canonical label derived from the primary type for UI / config matching.
|
|
||||||
std::string type_label() const { return plugin_capability_type_to_string(primary_capability_type()); }
|
|
||||||
|
|
||||||
std::string normalized_error() const
|
std::string normalized_error() const
|
||||||
{
|
{
|
||||||
@@ -159,8 +153,6 @@ inline void apply_plugin_metadata_fallbacks(PluginDescriptor& target, const Plug
|
|||||||
target.author = fallback.author;
|
target.author = fallback.author;
|
||||||
if (target.version.empty())
|
if (target.version.empty())
|
||||||
target.version = fallback.version;
|
target.version = fallback.version;
|
||||||
if (target.capability_types.empty())
|
|
||||||
target.capability_types = fallback.capability_types;
|
|
||||||
if (target.entry_package.empty())
|
if (target.entry_package.empty())
|
||||||
target.entry_package = fallback.entry_package;
|
target.entry_package = fallback.entry_package;
|
||||||
if (target.dependencies.empty())
|
if (target.dependencies.empty())
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
#include "PluginFsUtils.hpp"
|
#include "PluginFsUtils.hpp"
|
||||||
|
|
||||||
|
#include "PythonFileUtils.hpp"
|
||||||
#include "libslic3r/Utils.hpp"
|
#include "libslic3r/Utils.hpp"
|
||||||
|
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include "PluginAuditManager.hpp"
|
#include "PluginAuditManager.hpp"
|
||||||
|
|
||||||
@@ -107,4 +110,148 @@ bool delete_plugin_root(const boost::filesystem::path& resolved_root,
|
|||||||
return true;
|
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
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -32,4 +32,20 @@ bool delete_plugin_root(const boost::filesystem::path& resolved_root,
|
|||||||
const std::string& plugin_id,
|
const std::string& plugin_id,
|
||||||
std::string& error);
|
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
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
namespace Slic3r::plugin_hooks {
|
namespace Slic3r::plugin_hooks {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -25,12 +26,16 @@ void install_capability_resolver()
|
|||||||
{
|
{
|
||||||
ConfigBase::set_resolve_capability_fn([](const std::string& cap_name, const std::string& cap_type) {
|
ConfigBase::set_resolve_capability_fn([](const std::string& cap_name, const std::string& cap_type) {
|
||||||
PluginManager& plugin_mgr = PluginManager::instance();
|
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)
|
if (!plugin_cap)
|
||||||
return std::string();
|
return std::string();
|
||||||
|
|
||||||
PluginDescriptor descriptor;
|
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();
|
return std::string();
|
||||||
|
|
||||||
// Cloud plugins are resolved at runtime via the UUID in the middle field, so the first
|
// 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>(
|
execute_capabilities_from_refs<SlicingPipelinePluginCapability>(
|
||||||
*caps, plugs, PluginCapabilityType::SlicingPipeline,
|
*caps, plugs, PluginCapabilityType::SlicingPipeline,
|
||||||
[&](std::shared_ptr<SlicingPipelinePluginCapability> cap, const PluginCapabilityRef& ref) {
|
[&](std::shared_ptr<SlicingPipelinePluginCapability> cap, const PluginCapabilityRef& ref) {
|
||||||
|
const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
|
||||||
ExecutionResult r;
|
ExecutionResult r;
|
||||||
try {
|
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
|
// GIL is acquired per capability (not once for the whole dispatch) so it
|
||||||
// is released between capabilities.
|
// is released between capabilities.
|
||||||
PythonGILState gil;
|
PythonGILState gil;
|
||||||
|
if (!gil)
|
||||||
|
throw std::runtime_error("Python interpreter is shutting down");
|
||||||
// throw_if_canceled() is protected on PrintBase; canceled() is the public
|
// throw_if_canceled() is protected on PrintBase; canceled() is the public
|
||||||
// equivalent check (same cancel flag), so honor cancellation via it.
|
// equivalent check (same cancel flag), so honor cancellation via it.
|
||||||
if (print.canceled())
|
if (print.canceled())
|
||||||
@@ -78,8 +89,7 @@ void install_slicing_pipeline_hook()
|
|||||||
ctx.object = object;
|
ctx.object = object;
|
||||||
// hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params
|
// hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params
|
||||||
// (same plugin_key the capability was resolved by, so it always matches).
|
// (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 = plugin_settings;
|
||||||
ctx.params = PluginManager::instance().get_loader().get_plugin_settings(plugin_key);
|
|
||||||
r = cap->execute(ctx);
|
r = cap->execute(ctx);
|
||||||
} catch (const CanceledException&) {
|
} catch (const CanceledException&) {
|
||||||
throw; // cancellation must reach process(), never become a slicing error
|
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 <boost/filesystem/path.hpp>
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
|
||||||
#include <mutex>
|
|
||||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
|
||||||
#include <unordered_set>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <condition_variable>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstddef>
|
|
||||||
|
|
||||||
#include <pybind11/embed.h>
|
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
struct Plugin;
|
||||||
class PluginCatalog;
|
|
||||||
|
|
||||||
// A single capability materialized from a plugin package. The owning LoadedPlugin
|
|
||||||
// holds the descriptor and the Python module; capabilities only back-reference the
|
|
||||||
// package by plugin_key and cache their resolved name.
|
|
||||||
struct LoadedPluginCapability
|
|
||||||
{
|
|
||||||
std::shared_ptr<PluginCapabilityInterface> instance; // Materialized capability instance
|
|
||||||
std::string name; // Cached from instance->get_name() at load time
|
|
||||||
std::string plugin_key; // Owning package
|
|
||||||
PluginCapabilityType type = PluginCapabilityType::Unknown; // cached from instance->get_type() at load (GUI reads this without the GIL)
|
|
||||||
std::atomic<bool> enabled{true}; // logical enable/disable; disabled capabilities are skipped by consumers but stay loaded
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PluginCapabilityIdentifier
|
|
||||||
{
|
|
||||||
PluginCapabilityType type = PluginCapabilityType::Unknown;
|
|
||||||
std::string name;
|
|
||||||
std::string plugin_key; // owning package — makes the identity globally unique
|
|
||||||
|
|
||||||
bool operator==(const PluginCapabilityIdentifier& o) const
|
|
||||||
{ return type == o.type && name == o.name && plugin_key == o.plugin_key; }
|
|
||||||
};
|
|
||||||
|
|
||||||
// A loaded plugin package: one .py/.whl file → one descriptor + one module + N capabilities.
|
|
||||||
struct LoadedPlugin
|
|
||||||
{
|
|
||||||
PluginDescriptor descriptor;
|
|
||||||
PyObject* module = nullptr; // Python module object, shared by all capabilities
|
|
||||||
std::vector<PluginCapabilityIdentifier> capabilities;
|
|
||||||
|
|
||||||
LoadedPlugin() = default;
|
|
||||||
LoadedPlugin(const LoadedPlugin&) = delete;
|
|
||||||
LoadedPlugin& operator=(const LoadedPlugin&) = delete;
|
|
||||||
// Move transfers module ownership; the moved-from package must not Py_DECREF it.
|
|
||||||
LoadedPlugin(LoadedPlugin&& other) noexcept
|
|
||||||
: descriptor(std::move(other.descriptor)), module(other.module), capabilities(std::move(other.capabilities))
|
|
||||||
{ other.module = nullptr; }
|
|
||||||
LoadedPlugin& operator=(LoadedPlugin&& other) noexcept = delete;
|
|
||||||
|
|
||||||
~LoadedPlugin();
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|
||||||
template<> struct std::hash<Slic3r::PluginCapabilityIdentifier>
|
namespace Slic3r::plugin_loader {
|
||||||
{
|
bool load(const PluginDescriptor& descriptor,
|
||||||
std::size_t operator()(const Slic3r::PluginCapabilityIdentifier& id) const noexcept
|
bool skip_deps,
|
||||||
{
|
const std::vector<std::string>& capabilities_to_enable,
|
||||||
std::size_t h = std::hash<std::size_t>{}(static_cast<std::size_t>(id.type));
|
const std::function<std::string(const Plugin&)>& registry_precheck,
|
||||||
auto mix = [&h](std::size_t v) { h ^= v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2); };
|
Plugin& out,
|
||||||
mix(std::hash<std::string>{}(id.name));
|
std::string& error);
|
||||||
mix(std::hash<std::string>{}(id.plugin_key));
|
|
||||||
return h;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
// Install Python dependencies into the shared packages directory via the bundled uv. Blocks, with
|
||||||
{
|
// a 120 s cap.
|
||||||
public:
|
bool install_packages(const std::vector<std::string>& pkgs, std::string& error);
|
||||||
enum CallbackType {
|
|
||||||
Load,
|
|
||||||
Unload,
|
|
||||||
};
|
|
||||||
|
|
||||||
using PluginLoadInProgress = std::unordered_set<std::string>;
|
// Read a local .py/.whl package's metadata without installing it, and report whether a package is
|
||||||
using PluginLoadErrors = std::unordered_map<std::string, std::string>;
|
// 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>&)>>>;
|
} // namespace Slic3r::plugin_loader
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,28 +5,110 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <libslic3r/Config.hpp>
|
#include <libslic3r/Config.hpp>
|
||||||
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <pybind11/embed.h>
|
||||||
|
|
||||||
#include "CloudPluginService.hpp"
|
#include "CloudPluginService.hpp"
|
||||||
#include "PluginCatalog.hpp"
|
#include "PluginFsUtils.hpp"
|
||||||
#include "PluginLoader.hpp"
|
|
||||||
#include "PluginDescriptor.hpp"
|
#include "PluginDescriptor.hpp"
|
||||||
|
#include "PluginLoader.hpp"
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
class OrcaCloudServiceAgent;
|
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
|
class PluginManager
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
using PluginLifecycleCompleteFn = std::function<void(const std::string& /*plugin_key*/)>;
|
||||||
|
using CapabilityLifecycleFn = std::function<void(const PluginCapabilityId&)>;
|
||||||
|
|
||||||
static PluginManager& instance();
|
static PluginManager& instance();
|
||||||
|
|
||||||
~PluginManager();
|
~PluginManager();
|
||||||
@@ -37,35 +119,86 @@ public:
|
|||||||
// Stop discovery and unload Python plugin objects before Python finalizes.
|
// Stop discovery and unload Python plugin objects before Python finalizes.
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
|
||||||
// Discover and scan plugins from standard directories (manifest-only, no Python loading).
|
// Reject new plugin loads. Called early in app teardown, before shutdown() drains.
|
||||||
// Runs on a worker thread when async=true.
|
void set_shutting_down();
|
||||||
|
|
||||||
void discover_plugins(bool async = false, bool clear = false);
|
void discover_plugins(bool async = false, bool clear = false);
|
||||||
|
// Manually trigger a manifest-only rescan. Blocks until discovery is complete.
|
||||||
// fetches plugins from the cloud
|
|
||||||
void fetch_plugins_from_cloud(std::vector<std::string>* out_not_found = nullptr,
|
|
||||||
std::vector<std::string>* out_unauthorized = nullptr);
|
|
||||||
|
|
||||||
// Download and install a cloud plugin from its download_url.
|
|
||||||
// Returns an error string on failure (empty URL, network down, etc.).
|
|
||||||
// On success returns an empty string.
|
|
||||||
bool download_and_install_cloud_plugin(const std::string& plugin_key, const std::string& version, std::string& error);
|
|
||||||
bool subscribe_and_install_cloud_plugin(const std::string& plugin_key, std::string& error);
|
|
||||||
|
|
||||||
// Manually trigger manifest-only rescan of plugins. Blocks until discovery is complete.
|
|
||||||
void rescan_plugins();
|
void rescan_plugins();
|
||||||
|
|
||||||
PluginCatalog& get_catalog() { return m_catalog; }
|
bool is_discovery_complete() const;
|
||||||
const PluginCatalog& get_catalog() const { return m_catalog; }
|
bool is_discovery_in_progress() const;
|
||||||
PluginLoader& get_loader() { return m_loader; }
|
std::string get_discovery_error() const;
|
||||||
const PluginLoader& get_loader() const { return m_loader; }
|
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)); }
|
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, std::string& error);
|
||||||
bool install_plugin(const boost::filesystem::path& filepath, PluginDescriptor& plugin_descriptor, 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 set_plugin_error(const std::string& plugin_key, std::string error);
|
||||||
bool clear_plugin_error(const std::string& plugin_key);
|
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.
|
// If the version is empty, take the latest version.
|
||||||
bool update_cloud_plugin(const std::string& plugin_key, std::string& error, std::string version = "");
|
bool 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_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);
|
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:
|
private:
|
||||||
PluginManager() = default;
|
PluginManager() = default;
|
||||||
PluginManager(const PluginManager&) = delete;
|
PluginManager(const PluginManager&) = delete;
|
||||||
PluginManager& operator=(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 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 delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error);
|
||||||
bool keep_installed_plugin_as_local(const PluginDescriptor& plugin_descriptor, std::string& error);
|
bool keep_installed_plugin_as_local(const PluginDescriptor& plugin_descriptor, std::string& error);
|
||||||
|
|
||||||
bool m_initialized = false;
|
bool m_initialized = false;
|
||||||
CloudPluginService m_cloud_service;
|
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;
|
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,
|
void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
|
||||||
const ConfigOptionStrings* plugins,
|
const ConfigOptionStrings* plugins,
|
||||||
PluginCapabilityType type,
|
PluginCapabilityType type,
|
||||||
std::function<void(std::shared_ptr<T>, const PluginCapabilityRef&)> execute)
|
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,
|
// Log prefix derived from the capability type so each capability family (Printer connection,
|
||||||
// Slicing Pipeline, ...) tags its dispatch diagnostics with its own display name.
|
// 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 bool has_any = std::any_of(capabilities.values.begin(), capabilities.values.end(),
|
||||||
[](const std::string& s) { return !s.empty(); });
|
[](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";
|
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())
|
if (capability.empty())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
std::shared_ptr<LoadedPluginCapability> cap;
|
|
||||||
std::string cap_name, plugin_key;
|
|
||||||
|
|
||||||
std::optional<PluginCapabilityRef> ref;
|
std::optional<PluginCapabilityRef> ref;
|
||||||
if (plugins != nullptr) {
|
if (plugins != nullptr) {
|
||||||
for (const std::string& plugin_ref : plugins->values) {
|
for (const std::string& plugin_ref : plugins->values) {
|
||||||
@@ -135,25 +334,26 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
cap_name = ref->capability_name;
|
const std::string cap_name = ref->capability_name;
|
||||||
plugin_key = ref->uuid.empty() ? ref->name : ref->uuid;
|
const std::string plugin_key = ref->uuid.empty() ? ref->name : ref->uuid;
|
||||||
cap = plugin_mgr.get_loader().get_plugin_capability_by_name(plugin_key, type, cap_name);
|
|
||||||
|
|
||||||
|
// 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) {
|
if (!cap) {
|
||||||
BOOST_LOG_TRIVIAL(warning) << tag << ": no loaded capability '" << cap_name
|
BOOST_LOG_TRIVIAL(warning) << tag << ": no loaded capability '" << cap_name << "' for plugin '" << plugin_key << "'; skipping";
|
||||||
<< "' for plugin '" << plugin_key << "'; skipping";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!cap->enabled) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name
|
|
||||||
<< "' for plugin '" << plugin_key << "' is disabled; skipping";
|
|
||||||
continue;
|
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) {
|
if (!plugin_capability) {
|
||||||
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name
|
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name << "' (plugin_key=" << cap->audit_plugin_key()
|
||||||
<< "' (plugin_key=" << cap->plugin_key
|
|
||||||
<< ") is not a " << plugin_capability_type_to_string(type) << "; skipping";
|
<< ") is not a " << plugin_capability_type_to_string(type) << "; skipping";
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,15 +116,19 @@ std::string resolve_recovery_url(const PluginCapabilityRef& ref)
|
|||||||
// loaded or does not provide the capability.
|
// loaded or does not provide the capability.
|
||||||
static std::pair<bool, bool> loaded_capability_state(const std::string& plugin_key, const PluginCapabilityRef& ref)
|
static std::pair<bool, bool> loaded_capability_state(const std::string& plugin_key, const PluginCapabilityRef& ref)
|
||||||
{
|
{
|
||||||
bool present = false, enabled = false;
|
PluginManager& mgr = PluginManager::instance();
|
||||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
|
||||||
for (const auto& capability : loader.get_loaded_plugin_capabilities(plugin_key))
|
// Only a loaded package exposes capabilities; a discovered one carries their names in its
|
||||||
if (capability && capability->name == ref.capability_name) {
|
// descriptor but has materialized nothing.
|
||||||
present = true;
|
if (!mgr.is_plugin_loaded(plugin_key))
|
||||||
if (capability->enabled)
|
return {false, false};
|
||||||
enabled = true;
|
|
||||||
}
|
const auto capability = mgr.get_plugin_capability(plugin_key, ref.capability_name, PluginCapabilityType::Unknown,
|
||||||
return {present, enabled};
|
/*only_enabled=*/false);
|
||||||
|
if (!capability)
|
||||||
|
return {false, false};
|
||||||
|
|
||||||
|
return {true, capability->is_enabled()};
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh_missing_plugins(const PresetBundle& preset_bundle)
|
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)
|
if (manifest == nullptr)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PluginLoader& loader = PluginManager::instance().get_loader();
|
PluginManager& mgr = PluginManager::instance();
|
||||||
const PluginCatalog& catalog = PluginManager::instance().get_catalog();
|
|
||||||
for (const std::string& entry : manifest->values) {
|
for (const std::string& entry : manifest->values) {
|
||||||
const auto ref = parse_capability_ref(entry);
|
const auto ref = parse_capability_ref(entry);
|
||||||
if (!ref)
|
if (!ref)
|
||||||
@@ -175,9 +178,9 @@ void refresh_missing_plugins(Preset::Type type, const ConfigOptionStrings* manif
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
PluginDescriptor descriptor;
|
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 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)
|
const auto cap_state = installed ? loaded_capability_state(descriptor.plugin_key, *ref)
|
||||||
: std::pair<bool, bool>{false, false};
|
: std::pair<bool, bool>{false, false};
|
||||||
const bool cap_present = cap_state.first;
|
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.
|
// Use a friendly name for the progress message when the catalog already knows it.
|
||||||
std::string display_name = uuid;
|
std::string display_name = uuid;
|
||||||
PluginDescriptor known;
|
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;
|
display_name = known.name;
|
||||||
if (progress.on_plugin_begin)
|
if (progress.on_plugin_begin)
|
||||||
progress.on_plugin_begin(display_name, i, total);
|
progress.on_plugin_begin(display_name, i, total);
|
||||||
@@ -322,14 +325,13 @@ void resolve_missing_plugins(const std::vector<std::string>& refs, PluginInstall
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
PluginDescriptor descriptor;
|
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.");
|
report_install_failure(uuid + ": installed plugin was not found in the catalog.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
PluginLoader& loader = mgr.get_loader();
|
mgr.load_plugin(descriptor.plugin_key, false);
|
||||||
loader.load_plugin(mgr.get_catalog(), descriptor.plugin_key, false);
|
if (!mgr.wait_for_plugin_load(descriptor.plugin_key, std::chrono::minutes(5), error) ||
|
||||||
if (!loader.wait_for_plugin_load(descriptor.plugin_key, std::chrono::minutes(5), error) ||
|
!mgr.is_plugin_loaded(descriptor.plugin_key)) {
|
||||||
!loader.is_plugin_loaded(descriptor.plugin_key)) {
|
|
||||||
report_install_failure(descriptor.name + ": " + (error.empty() ? "plugin failed to load." : error));
|
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)
|
void resolve_inactive_plugins(const std::vector<std::string>& refs)
|
||||||
{
|
{
|
||||||
PluginManager& mgr = PluginManager::instance();
|
PluginManager& mgr = PluginManager::instance();
|
||||||
PluginCatalog& catalog = mgr.get_catalog();
|
|
||||||
|
|
||||||
// Group the requested capabilities by owning plugin so each plugin is loaded once with the full
|
// Group the requested capabilities by owning plugin so each plugin is loaded once with the full
|
||||||
// set to enable.
|
// set to enable.
|
||||||
@@ -353,7 +354,7 @@ void resolve_inactive_plugins(const std::vector<std::string>& refs)
|
|||||||
continue;
|
continue;
|
||||||
const std::string key = ref->uuid.empty() ? ref->name : ref->uuid;
|
const std::string key = ref->uuid.empty() ? ref->name : ref->uuid;
|
||||||
PluginDescriptor descriptor;
|
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;
|
continue;
|
||||||
by_plugin[descriptor.plugin_key].push_back(ref->capability_name);
|
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.
|
// 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::vector<std::pair<std::string, std::vector<std::string>>> work(by_plugin.begin(), by_plugin.end());
|
||||||
std::thread([work = std::move(work)]() {
|
std::thread([work = std::move(work)]() {
|
||||||
PluginManager& mgr = PluginManager::instance();
|
PluginManager& mgr = PluginManager::instance();
|
||||||
PluginLoader& loader = mgr.get_loader();
|
|
||||||
PluginCatalog& catalog = mgr.get_catalog();
|
|
||||||
for (auto& [plugin_key, capabilities] : work) {
|
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;
|
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([]() {
|
GUI::wxGetApp().CallAfter([]() {
|
||||||
if (GUI::Plater* plater = GUI::wxGetApp().plater())
|
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);
|
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
|
} // 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 create_full_ref(const PluginCapabilityRef& ref);
|
||||||
std::string resolve_recovery_url(const PluginCapabilityRef& ref);
|
std::string resolve_recovery_url(const PluginCapabilityRef& ref);
|
||||||
|
|
||||||
bool check_capability_in_use(const std::string& capability_refs);
|
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <pybind11/embed.h>
|
#include <pybind11/embed.h>
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
#include "PythonPluginInterface.hpp"
|
#include "PythonPluginInterface.hpp"
|
||||||
#include "PythonInterpreter.hpp"
|
#include "PythonInterpreter.hpp"
|
||||||
@@ -20,29 +21,32 @@
|
|||||||
// Logs (and rethrows) a Python exception from a pybind11 override call, preserving the
|
// 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
|
// traceback. Internal helper shared by the public macros below and by trampolines that
|
||||||
// manage their own audit scope (e.g. the G-code plugin).
|
// manage their own audit scope (e.g. the G-code plugin).
|
||||||
#define ORCA_PY_LOGGED_OVERRIDE_BODY(override_call) \
|
#define ORCA_PY_LOGGED_OVERRIDE_BODY(override_call) \
|
||||||
try { \
|
try { \
|
||||||
override_call; \
|
override_call; \
|
||||||
} catch (pybind11::error_already_set & err) { \
|
} catch (pybind11::error_already_set & err) { \
|
||||||
::Slic3r::log_python_exception_keep(err); \
|
::Slic3r::log_python_exception_keep(err); \
|
||||||
throw; \
|
throw; \
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call
|
// Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call
|
||||||
// when this trampoline instance carries a non-empty audit plugin key. Declares a local
|
// when this trampoline instance carries a non-empty audit plugin key. Declares a local
|
||||||
// `_orca_audit_scope`.
|
// `_orca_audit_scope`.
|
||||||
#define ORCA_PY_AUDIT_SCOPE(mode) \
|
#define ORCA_PY_AUDIT_SCOPE(mode) \
|
||||||
std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \
|
std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \
|
||||||
if (const std::string& _orca_audit_key = this->audit_plugin_key(); \
|
if (const std::string& _orca_audit_key = this->audit_plugin_key(); !_orca_audit_key.empty()) \
|
||||||
!_orca_audit_key.empty()) \
|
_orca_audit_scope.emplace(_orca_audit_key, mode)
|
||||||
_orca_audit_scope.emplace(_orca_audit_key, mode)
|
|
||||||
|
|
||||||
#define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \
|
#define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \
|
||||||
do { \
|
do { \
|
||||||
ORCA_PY_AUDIT_SCOPE(mode); \
|
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); \
|
||||||
if (_orca_audit_scope) \
|
::Slic3r::PythonGILState _orca_python_gil; \
|
||||||
audit_setup(); \
|
if (!_orca_python_gil) \
|
||||||
ORCA_PY_LOGGED_OVERRIDE_BODY(override_macro(ret, base, name, ##__VA_ARGS__)); \
|
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)
|
} while (0)
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
@@ -54,36 +58,23 @@ public:
|
|||||||
// get_name is required on all capabilities — Python subclass must implement it.
|
// get_name is required on all capabilities — Python subclass must implement it.
|
||||||
std::string get_name() const override
|
std::string get_name() const override
|
||||||
{
|
{
|
||||||
ORCA_PY_OVERRIDE_AUDITED(
|
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name);
|
||||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
|
||||||
[] {},
|
|
||||||
PYBIND11_OVERRIDE_PURE,
|
|
||||||
std::string,
|
|
||||||
Base,
|
|
||||||
get_name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All plugins may define their own on_load/unload functions.
|
// All plugins may define their own on_load/unload functions.
|
||||||
void on_load() override
|
void on_load() override
|
||||||
{
|
{
|
||||||
ORCA_PY_OVERRIDE_AUDITED(
|
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_load);
|
||||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
|
||||||
[] {},
|
|
||||||
PYBIND11_OVERRIDE,
|
|
||||||
void,
|
|
||||||
Base,
|
|
||||||
on_load);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void on_unload() override
|
void on_unload() override
|
||||||
{
|
{
|
||||||
ORCA_PY_OVERRIDE_AUDITED(
|
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_unload);
|
||||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
}
|
||||||
[] {},
|
|
||||||
PYBIND11_OVERRIDE,
|
void on_cancelled() override
|
||||||
void,
|
{
|
||||||
Base,
|
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
|
||||||
on_unload);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,11 +88,7 @@ public:
|
|||||||
PluginCapabilityType get_type() const override
|
PluginCapabilityType get_type() const override
|
||||||
{
|
{
|
||||||
ORCA_PY_OVERRIDE_AUDITED(
|
ORCA_PY_OVERRIDE_AUDITED(
|
||||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface,
|
||||||
[] {},
|
|
||||||
PYBIND11_OVERRIDE,
|
|
||||||
PluginCapabilityType,
|
|
||||||
PluginCapabilityInterface,
|
|
||||||
get_type);
|
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)
|
void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry)
|
||||||
{
|
{
|
||||||
namespace fs = boost::filesystem;
|
PluginInstallState state;
|
||||||
const fs::path sidecar_path = plugin_dir / ".install_state.json";
|
if (!read_install_state(plugin_dir, state))
|
||||||
|
|
||||||
if (!fs::exists(sidecar_path) || !fs::is_regular_file(sidecar_path))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
boost::nowide::ifstream f(sidecar_path.string());
|
// The cloud identity and the persisted installed version are read back. plugin_key
|
||||||
if (!f)
|
// is always derived by the catalog scan (filename for local, the cloud uuid for
|
||||||
return;
|
// 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 {
|
// Package-level auto-load flag only. The per-capability enable flags stay in the sidecar: a
|
||||||
nlohmann::json state = nlohmann::json::parse(f, nullptr, false, true);
|
// capability has no existence — and so no state — until it is materialized, at which point the
|
||||||
if (state.is_discarded() || !state.is_object())
|
// loader seeds the flag onto the capability itself.
|
||||||
return;
|
entry.enabled = state.enabled;
|
||||||
|
|
||||||
// The cloud identity and the persisted installed version are read back. plugin_key
|
|
||||||
// is always derived by the catalog scan (filename for local, the cloud uuid for
|
|
||||||
// cloud), so it is not read from the sidecar. installed_version is the source of
|
|
||||||
// truth for a cloud plugin's installed version: it records the version fetched from
|
|
||||||
// the cloud at install time, independent of the (possibly stale) manifest/PEP723
|
|
||||||
// header that scan_directory parses into entry.version.
|
|
||||||
if (state.contains("installed_version") && state["installed_version"].is_string())
|
|
||||||
entry.installed_version = state["installed_version"].get<std::string>();
|
|
||||||
if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string()) {
|
|
||||||
const std::string cloud_uuid = state["cloud_uuid"].get<std::string>();
|
|
||||||
if (!cloud_uuid.empty())
|
|
||||||
entry.cloud = CloudPluginState{cloud_uuid, true, false, false};
|
|
||||||
}
|
|
||||||
} catch (...) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out)
|
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)
|
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, {});
|
return write_install_state(plugin_dir, entry, true, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,17 +15,77 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
|
#include <mutex>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Slic3r {
|
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)
|
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
|
// 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
|
// local destroyed by stack unwinding before this catch runs. Touching Python
|
||||||
// state below needs the GIL. PyGILState_Ensure is reentrant — harmless if held.
|
// state below needs the GIL. PyGILState_Ensure is reentrant — harmless if held.
|
||||||
PythonGILState gil;
|
PythonGILState gil;
|
||||||
|
if (!gil)
|
||||||
|
return;
|
||||||
|
|
||||||
// Non-destructive: print the traceback to sys.stderr (tee'd to the session log)
|
// 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++
|
// 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
|
#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");
|
PyObject* sys_path = PySys_GetObject("path");
|
||||||
if (!sys_path || !PyList_Check(sys_path)) {
|
if (!sys_path || !PyList_Check(sys_path)) {
|
||||||
error = "Python sys.path is not available";
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inserted)
|
||||||
|
*inserted = true;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,94 +453,23 @@ std::string PythonInterpreter::bundled_uv_path()
|
|||||||
|
|
||||||
bool PythonInterpreter::initialize()
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_last_error.clear();
|
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 {
|
try {
|
||||||
// Set Python home to the bundled Python installation
|
// Set Python home to the bundled Python installation
|
||||||
// This is critical for finding the standard library (encodings module, etc.)
|
// This is critical for finding the standard library (encodings module, etc.)
|
||||||
|
|
||||||
namespace fs = boost::filesystem;
|
namespace fs = boost::filesystem;
|
||||||
std::string python_home;
|
const std::string python_home = find_bundled_python_home().string();
|
||||||
const auto valid_python_home = [](const fs::path& candidate) {
|
|
||||||
#ifdef _WIN32
|
|
||||||
return fs::exists(candidate / "Lib" / "encodings") &&
|
|
||||||
(fs::exists(candidate / PYTHON_DLL) || fs::exists(candidate / PYTHON_DEBUG_DLL));
|
|
||||||
#else
|
|
||||||
return fs::exists(candidate / "lib" / PYTHON_STDLIB_DIR / "encodings");
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
|
|
||||||
// Determine Python home based on application structure
|
|
||||||
// Python is bundled at different locations depending on platform and build type
|
|
||||||
|
|
||||||
// Strategy 1: Platform-specific bundled locations (highest priority)
|
|
||||||
#ifdef __APPLE__
|
|
||||||
// macOS app bundle: OrcaSlicer.app/Contents/MacOS/python
|
|
||||||
// (resources_dir is Contents/Resources, so go up and into MacOS)
|
|
||||||
fs::path bundle_python = fs::path(resources_dir()).parent_path() / "MacOS" / "python";
|
|
||||||
if (valid_python_home(bundle_python)) {
|
|
||||||
python_home = bundle_python.string();
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Found Python in macOS app bundle: " << python_home;
|
|
||||||
}
|
|
||||||
#elif defined(_WIN32)
|
|
||||||
fs::path exe_python = boost::dll::program_location().parent_path() / "python";
|
|
||||||
if (valid_python_home(exe_python)) {
|
|
||||||
python_home = exe_python.string();
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Found Python next to Windows executable: " << python_home;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// Linux: typically in ../lib or ../share relative to binary
|
|
||||||
fs::path linux_python = fs::path(resources_dir()).parent_path() / "lib" / "python";
|
|
||||||
if (valid_python_home(linux_python)) {
|
|
||||||
python_home = linux_python.string();
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Found Python in Linux install: " << python_home;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Strategy 2: Configured development dependency directory.
|
|
||||||
if (python_home.empty()) {
|
|
||||||
fs::path configured_python = ORCA_BUNDLED_PYTHON_ROOT;
|
|
||||||
if (!configured_python.empty() && valid_python_home(configured_python)) {
|
|
||||||
python_home = configured_python.string();
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Found Python in configured bundled path: " << python_home;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 3: Development build directory from runtime environment.
|
|
||||||
if (python_home.empty()) {
|
|
||||||
const char* prefix_path = std::getenv("CMAKE_PREFIX_PATH");
|
|
||||||
if (prefix_path && std::strlen(prefix_path) > 0) {
|
|
||||||
fs::path libpython = fs::path(prefix_path) / "libpython";
|
|
||||||
if (valid_python_home(libpython)) {
|
|
||||||
python_home = libpython.string();
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Found Python in CMAKE_PREFIX_PATH: " << python_home;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 3: Check resources directory (alternate bundling location)
|
|
||||||
if (python_home.empty()) {
|
|
||||||
fs::path res_python = fs::path(resources_dir()) / "python";
|
|
||||||
if (valid_python_home(res_python)) {
|
|
||||||
python_home = res_python.string();
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Found Python in resources directory: " << python_home;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 4: Check data_dir (user configuration directory)
|
|
||||||
#ifndef _WIN32
|
|
||||||
if (python_home.empty()) {
|
|
||||||
fs::path data_python = fs::path(data_dir()) / "python";
|
|
||||||
if (valid_python_home(data_python)) {
|
|
||||||
python_home = data_python.string();
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Found Python in data directory: " << python_home;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (python_home.empty()) {
|
if (python_home.empty()) {
|
||||||
m_last_error = "Could not locate bundled Python installation";
|
m_last_error = "Could not locate bundled Python installation";
|
||||||
@@ -493,6 +488,8 @@ bool PythonInterpreter::initialize()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "Found bundled Python home: " << python_home;
|
||||||
|
|
||||||
// Verify Python standard library directory exists
|
// Verify Python standard library directory exists
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
fs::path python_lib = fs::path(python_home) / "Lib";
|
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
|
// Set Python home - this is the prefix where Python libraries are located
|
||||||
PyConfig config;
|
PyConfig config;
|
||||||
PyConfig_InitPythonConfig(&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;
|
BOOST_LOG_TRIVIAL(debug) << "Calling PyConfig_SetBytesString with home=" << python_home;
|
||||||
PyStatus status = PyConfig_SetBytesString(&config, &config.home, python_home.c_str());
|
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.
|
// background-thread exceptions) to <data_dir>/log/python_*.log.
|
||||||
install_python_stderr_redirect();
|
install_python_stderr_redirect();
|
||||||
|
|
||||||
m_initialized = true;
|
|
||||||
|
|
||||||
// Release the GIL so other threads can acquire it via PyGILState_Ensure.
|
// 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.
|
// Without this, calls from background threads will block trying to acquire the GIL.
|
||||||
m_main_thread_state = PyEval_SaveThread();
|
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";
|
BOOST_LOG_TRIVIAL(debug) << "Main thread released Python GIL after initialization";
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -650,7 +650,8 @@ PythonInterpreter::~PythonInterpreter() { shutdown(); }
|
|||||||
|
|
||||||
void 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;
|
return;
|
||||||
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Python interpreter shutdown enter";
|
BOOST_LOG_TRIVIAL(info) << "Python interpreter shutdown enter";
|
||||||
@@ -668,80 +669,305 @@ void PythonInterpreter::shutdown()
|
|||||||
m_interpreter.reset();
|
m_interpreter.reset();
|
||||||
BOOST_LOG_TRIVIAL(info) << "Python interpreter finalized";
|
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";
|
error = "Python interpreter not initialized";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
PythonGILState gil;
|
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");
|
PyObject* sys_path = PySys_GetObject("path");
|
||||||
if (!sys_path || !PyList_Check(sys_path)) {
|
if (!sys_path || !PyList_Check(sys_path)) {
|
||||||
error = "Python sys.path is not available";
|
PyErr_Clear();
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObjectPtr py_path(PyUnicode_DecodeFSDefault(path.c_str()));
|
for (const std::string& path : paths) {
|
||||||
if (!py_path) {
|
auto users = m_plugin_path_users.find(path);
|
||||||
error = "Failed to decode path for Python sys.path: " + path;
|
if (users == m_plugin_path_users.end())
|
||||||
PyErr_Clear();
|
continue;
|
||||||
return false;
|
|
||||||
|
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) {
|
if (!plugin_modules)
|
||||||
error = "Python interpreter not initialized";
|
return;
|
||||||
return false;
|
|
||||||
|
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;
|
PythonGILState gil;
|
||||||
|
if (!gil)
|
||||||
|
return;
|
||||||
|
|
||||||
PyObject* main_module = PyImport_AddModule("__main__");
|
remove_plugin_modules_locked(plugin_modules);
|
||||||
if (!main_module) {
|
if (plugin_modules.empty())
|
||||||
error = "Failed to get __main__ module";
|
remove_module_tree_locked(module_name);
|
||||||
return false;
|
remove_plugin_sys_paths_locked(plugin_paths);
|
||||||
}
|
Py_XDECREF(module);
|
||||||
|
|
||||||
PyObject* global_dict = PyModule_GetDict(main_module);
|
|
||||||
PyObjectPtr result(PyRun_String(code.c_str(), Py_file_input, global_dict, global_dict));
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
PyObject *ptype, *pvalue, *ptraceback;
|
|
||||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
|
||||||
error = format_python_error(ptype, pvalue, ptraceback);
|
|
||||||
Py_XDECREF(ptype);
|
|
||||||
Py_XDECREF(pvalue);
|
|
||||||
Py_XDECREF(ptraceback);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* PythonInterpreter::load_module_from_file(const std::string& file_path, std::string& error)
|
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";
|
error = "Python interpreter not initialized";
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -755,49 +981,27 @@ PyObject* PythonInterpreter::load_module_from_file(const std::string& file_path,
|
|||||||
}
|
}
|
||||||
|
|
||||||
PythonGILState gil;
|
PythonGILState gil;
|
||||||
|
if (!gil) {
|
||||||
|
error = "Python interpreter is shutting down";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// Add the directory to sys.path
|
// Add the directory to sys.path
|
||||||
fs::path dir_path = path.parent_path();
|
fs::path dir_path = path.parent_path();
|
||||||
std::string module_name = path.stem().string();
|
std::string module_name = path.stem().string();
|
||||||
|
|
||||||
PyObjectPtr sys(PyImport_ImportModule("sys"));
|
const std::string dir = dir_path.string();
|
||||||
if (!sys) {
|
if (!add_plugin_sys_path_locked(dir, error))
|
||||||
error = "Failed to import sys module";
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
if (plugin_paths)
|
||||||
|
plugin_paths->push_back(dir);
|
||||||
PyObject* sys_path = PyObject_GetAttrString(sys.get(), "path");
|
|
||||||
if (!sys_path) {
|
|
||||||
error = "Failed to get sys.path";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObjectPtr dir_str(PyUnicode_FromString(dir_path.string().c_str()));
|
|
||||||
if (!dir_str) {
|
|
||||||
Py_DECREF(sys_path);
|
|
||||||
error = "Failed to create directory string";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PyList_Insert(sys_path, 0, dir_str.get()) < 0) {
|
|
||||||
Py_DECREF(sys_path);
|
|
||||||
error = "Failed to add directory to sys.path";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
Py_DECREF(sys_path);
|
|
||||||
|
|
||||||
// Ensure module is re-imported fresh by removing any cached instance.
|
// Ensure module is re-imported fresh by removing any cached instance.
|
||||||
if (PyObject* modules = PyImport_GetModuleDict()) {
|
remove_module_tree_locked(module_name);
|
||||||
if (PyDict_GetItemString(modules, module_name.c_str())) {
|
|
||||||
if (PyDict_DelItemString(modules, module_name.c_str()) != 0) {
|
|
||||||
PyErr_Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import the module
|
// Import the module
|
||||||
PyObject* module = PyImport_ImportModule(module_name.c_str());
|
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) {
|
if (!module) {
|
||||||
PyObject *ptype, *pvalue, *ptraceback;
|
PyObject *ptype, *pvalue, *ptraceback;
|
||||||
PyErr_Fetch(&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;
|
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";
|
error = "Python interpreter not initialized";
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -827,43 +1035,21 @@ PyObject* PythonInterpreter::load_module_from_directory(const std::string& dir_p
|
|||||||
}
|
}
|
||||||
|
|
||||||
PythonGILState gil;
|
PythonGILState gil;
|
||||||
|
if (!gil) {
|
||||||
PyObjectPtr sys(PyImport_ImportModule("sys"));
|
error = "Python interpreter is shutting down";
|
||||||
if (!sys) {
|
|
||||||
error = "Failed to import sys module";
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* sys_path = PyObject_GetAttrString(sys.get(), "path");
|
const std::string directory = dir.string();
|
||||||
if (!sys_path) {
|
if (!add_plugin_sys_path_locked(directory, error))
|
||||||
error = "Failed to get sys.path";
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
if (plugin_paths)
|
||||||
|
plugin_paths->push_back(directory);
|
||||||
|
|
||||||
PyObjectPtr dir_str(PyUnicode_FromString(dir.string().c_str()));
|
remove_module_tree_locked(pkg_name);
|
||||||
if (!dir_str) {
|
|
||||||
Py_DECREF(sys_path);
|
|
||||||
error = "Failed to create directory string";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PyList_Insert(sys_path, 0, dir_str.get()) < 0) {
|
|
||||||
Py_DECREF(sys_path);
|
|
||||||
error = "Failed to add directory to sys.path";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
Py_DECREF(sys_path);
|
|
||||||
|
|
||||||
if (PyObject* modules = PyImport_GetModuleDict()) {
|
|
||||||
if (PyDict_GetItemString(modules, pkg_name.c_str())) {
|
|
||||||
if (PyDict_DelItemString(modules, pkg_name.c_str()) != 0) {
|
|
||||||
PyErr_Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObject* module = PyImport_ImportModule(pkg_name.c_str());
|
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) {
|
if (!module) {
|
||||||
PyObject *ptype, *pvalue, *ptraceback;
|
PyObject *ptype, *pvalue, *ptraceback;
|
||||||
PyErr_Fetch(&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;
|
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";
|
error = "Python interpreter not initialized";
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -900,98 +1090,7 @@ PyObject* PythonInterpreter::load_module_from_whl(const std::string& file_path,
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
return load_module_from_directory(extract_dir.string(), pkg_name, error);
|
return load_module_from_directory(extract_dir.string(), pkg_name, error, plugin_paths, plugin_modules);
|
||||||
}
|
|
||||||
|
|
||||||
bool PythonInterpreter::call_function(
|
|
||||||
PyObject* module, const std::string& function_name, const std::string& arg, std::string& result, std::string& error)
|
|
||||||
{
|
|
||||||
if (!m_initialized || !module) {
|
|
||||||
error = "Python interpreter not initialized or module is null";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
PythonGILState gil;
|
|
||||||
|
|
||||||
PyObject* func = PyObject_GetAttrString(module, function_name.c_str());
|
|
||||||
if (!func || !PyCallable_Check(func)) {
|
|
||||||
Py_XDECREF(func);
|
|
||||||
error = "Function '" + function_name + "' not found or not callable";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObjectPtr args(PyTuple_New(1));
|
|
||||||
PyObjectPtr arg_str(PyUnicode_FromString(arg.c_str()));
|
|
||||||
PyTuple_SetItem(args.get(), 0, arg_str.release());
|
|
||||||
|
|
||||||
PyObjectPtr py_result(PyObject_CallObject(func, args.get()));
|
|
||||||
Py_DECREF(func);
|
|
||||||
|
|
||||||
if (!py_result) {
|
|
||||||
PyObject *ptype, *pvalue, *ptraceback;
|
|
||||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
|
||||||
error = "Function call failed: " + format_python_error(ptype, pvalue, ptraceback);
|
|
||||||
Py_XDECREF(ptype);
|
|
||||||
Py_XDECREF(pvalue);
|
|
||||||
Py_XDECREF(ptraceback);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = py_object_to_string(py_result.get());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PythonInterpreter::call_function_no_args(PyObject* module, const std::string& function_name, std::string& result, std::string& error)
|
|
||||||
{
|
|
||||||
if (!m_initialized || !module) {
|
|
||||||
error = "Python interpreter not initialized or module is null";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
PythonGILState gil;
|
|
||||||
|
|
||||||
PyObject* func = PyObject_GetAttrString(module, function_name.c_str());
|
|
||||||
if (!func || !PyCallable_Check(func)) {
|
|
||||||
Py_XDECREF(func);
|
|
||||||
error = "Function '" + function_name + "' not found or not callable";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObjectPtr py_result(PyObject_CallObject(func, nullptr));
|
|
||||||
Py_DECREF(func);
|
|
||||||
|
|
||||||
if (!py_result) {
|
|
||||||
PyObject *ptype, *pvalue, *ptraceback;
|
|
||||||
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
|
|
||||||
error = "Function call failed: " + format_python_error(ptype, pvalue, ptraceback);
|
|
||||||
Py_XDECREF(ptype);
|
|
||||||
Py_XDECREF(pvalue);
|
|
||||||
Py_XDECREF(ptraceback);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = py_object_to_string(py_result.get());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string PythonInterpreter::py_object_to_string(PyObject* obj)
|
|
||||||
{
|
|
||||||
if (!obj) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PyUnicode_Check(obj)) {
|
|
||||||
const char* str = PyUnicode_AsUTF8(obj);
|
|
||||||
return str ? std::string(str) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObjectPtr str_obj(PyObject_Str(obj));
|
|
||||||
if (str_obj) {
|
|
||||||
const char* str = PyUnicode_AsUTF8(str_obj.get());
|
|
||||||
return str ? std::string(str) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -3,9 +3,13 @@
|
|||||||
|
|
||||||
#include <Python.h>
|
#include <Python.h>
|
||||||
#include <pytypedefs.h>
|
#include <pytypedefs.h>
|
||||||
#include <string>
|
#include <atomic>
|
||||||
#include <memory>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <shared_mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
#include "libslic3r/libslic3r.h"
|
#include "libslic3r/libslic3r.h"
|
||||||
|
|
||||||
namespace pybind11 {
|
namespace pybind11 {
|
||||||
@@ -15,6 +19,8 @@ class error_already_set;
|
|||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
|
class PythonRuntimeLease;
|
||||||
|
|
||||||
// Print a Python exception's full traceback to sys.stderr (tee'd to the session
|
// Print a Python exception's full traceback to sys.stderr (tee'd to the session
|
||||||
// log) WITHOUT consuming err.
|
// log) WITHOUT consuming err.
|
||||||
//
|
//
|
||||||
@@ -37,7 +43,11 @@ public:
|
|||||||
bool initialize();
|
bool initialize();
|
||||||
|
|
||||||
// Check if interpreter is initialized
|
// 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; }
|
const std::string& last_error() const { return m_last_error; }
|
||||||
|
|
||||||
@@ -56,56 +66,116 @@ public:
|
|||||||
// Finalize the Python interpreter.
|
// Finalize the Python interpreter.
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
|
||||||
// Add a filesystem path to sys.path if not already present.
|
// Add a path owned by one plugin load. The path is reference-counted across plugins and is
|
||||||
bool add_sys_path(const std::string& path, std::string& error);
|
// removed when the final plugin using it is unloaded.
|
||||||
|
bool add_plugin_sys_path(const std::string& path, std::string& error);
|
||||||
// Execute a Python string and return result
|
|
||||||
bool execute_string(const std::string& code, std::string& error);
|
|
||||||
|
|
||||||
// Load a Python module from file path
|
// Load a Python module from file path
|
||||||
PyObject* load_module_from_file(const std::string& file_path, std::string& error);
|
PyObject* load_module_from_file(const std::string& file_path,
|
||||||
PyObject* load_module_from_whl(const std::string& whl_path, const std::string& pkg_name, std::string& error);
|
std::string& error,
|
||||||
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);
|
||||||
|
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
|
// Remove the complete module namespace and plugin-owned paths, then release the root module
|
||||||
bool call_function(PyObject* module, const std::string& function_name,
|
// reference. Safe to call with a null module when a load failed after adding paths.
|
||||||
const std::string& arg, std::string& result, std::string& error);
|
void unload_module(PyObject* module,
|
||||||
|
const std::string& module_name,
|
||||||
// Call a Python function with no arguments, return string result
|
const std::vector<std::string>& plugin_paths,
|
||||||
bool call_function_no_args(PyObject* module, const std::string& function_name,
|
const std::vector<std::string>& plugin_modules);
|
||||||
std::string& result, std::string& error);
|
|
||||||
|
|
||||||
// Helper to get string from Python object
|
|
||||||
static std::string py_object_to_string(PyObject* obj);
|
|
||||||
|
|
||||||
// Destructor finalizes Python if shutdown() was not called explicitly.
|
// Destructor finalizes Python if shutdown() was not called explicitly.
|
||||||
~PythonInterpreter();
|
~PythonInterpreter();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend class PythonRuntimeLease;
|
||||||
|
|
||||||
PythonInterpreter() = default;
|
PythonInterpreter() = default;
|
||||||
PythonInterpreter(const PythonInterpreter&) = delete;
|
PythonInterpreter(const PythonInterpreter&) = delete;
|
||||||
PythonInterpreter& operator=(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
|
PyThreadState* m_main_thread_state = nullptr; // thread state saved after releasing GIL post-initialize
|
||||||
std::unique_ptr<pybind11::scoped_interpreter> m_interpreter;
|
std::unique_ptr<pybind11::scoped_interpreter> m_interpreter;
|
||||||
std::string m_last_error;
|
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)
|
// RAII helper for Python GIL (Global Interpreter Lock)
|
||||||
class PythonGILState
|
class PythonGILState
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PythonGILState() {
|
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() {
|
~PythonGILState() {
|
||||||
PyGILState_Release(m_state);
|
if (m_acquired)
|
||||||
|
PyGILState_Release(m_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
explicit operator bool() const { return m_acquired; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
PyGILState_STATE m_state;
|
PythonRuntimeLease m_runtime_lease;
|
||||||
|
PyGILState_STATE m_state{};
|
||||||
|
bool m_acquired = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// RAII helper for Python object references
|
// RAII helper for Python object references
|
||||||
|
|||||||
@@ -44,19 +44,35 @@ struct PluginInstanceHandle
|
|||||||
~PluginInstanceHandle()
|
~PluginInstanceHandle()
|
||||||
{
|
{
|
||||||
if (keep_alive) {
|
if (keep_alive) {
|
||||||
if (Py_IsInitialized()) {
|
// Dropping a py::object decrefs the Python object, so acquire a runtime lease and
|
||||||
// Dropping a py::object decrefs the Python object, so reacquire the GIL.
|
// the GIL. If shutdown has already won the lease, intentionally abandon the wrapper.
|
||||||
PythonGILState gil;
|
PythonGILState gil;
|
||||||
|
if (gil) {
|
||||||
keep_alive = py::object();
|
keep_alive = py::object();
|
||||||
} else {
|
} 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) 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
|
} // namespace
|
||||||
|
|
||||||
PythonPluginBridge& PythonPluginBridge::instance()
|
PythonPluginBridge& PythonPluginBridge::instance()
|
||||||
@@ -68,6 +84,10 @@ PythonPluginBridge& PythonPluginBridge::instance()
|
|||||||
void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
|
void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
|
||||||
{
|
{
|
||||||
PythonGILState gil;
|
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;
|
BOOST_LOG_TRIVIAL(info) << "Beginning Python plugin capture for key " << plugin_key;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
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)
|
std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& plugin_key, std::string& error)
|
||||||
{
|
{
|
||||||
PythonGILState gil;
|
PythonGILState gil;
|
||||||
|
if (!gil) {
|
||||||
|
error = "Python interpreter is shutting down";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << plugin_key;
|
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
|
// 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;
|
PythonGILState gil;
|
||||||
BOOST_LOG_TRIVIAL(warning) << "Cancelling Python plugin capture for key " << plugin_key;
|
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);
|
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||||
// Import or dependency setup failed before finalization. Drop anything the module
|
// 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()
|
void PythonPluginBridge::clear_pending_captures()
|
||||||
{
|
{
|
||||||
if (!Py_IsInitialized()) {
|
PythonGILState gil;
|
||||||
|
if (!gil) {
|
||||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||||
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size()
|
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size()
|
||||||
<< " pending Python plugin capture(s) without Python interpreter";
|
<< " pending Python plugin capture(s) without Python interpreter";
|
||||||
@@ -271,9 +303,6 @@ void PythonPluginBridge::clear_pending_captures()
|
|||||||
return;
|
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);
|
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||||
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
|
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
|
||||||
g_pending_capabilities.clear();
|
g_pending_capabilities.clear();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef slic3r_PythonPluginInterface_hpp_
|
#ifndef slic3r_PythonPluginInterface_hpp_
|
||||||
#define slic3r_PythonPluginInterface_hpp_
|
#define slic3r_PythonPluginInterface_hpp_
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
@@ -96,22 +97,77 @@ struct ExecutionResult
|
|||||||
class PluginCapabilityInterface
|
class PluginCapabilityInterface
|
||||||
{
|
{
|
||||||
public:
|
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 ~PluginCapabilityInterface() = default;
|
||||||
|
|
||||||
virtual std::string get_name() const = 0; // required — overridden in Python
|
// DO NOT CALL THESE OUTSIDE THE LOADER'S MATERIALIZATION BLOCK. get_name() is pure virtual and
|
||||||
virtual PluginCapabilityType get_type() const { return PluginCapabilityType::Unknown; } // optional — typed bases override
|
// 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_load() {}
|
||||||
virtual void on_unload() {}
|
virtual void on_unload() {}
|
||||||
|
virtual void on_cancelled() {}
|
||||||
|
|
||||||
// C++-only audit identity (never exposed to Python). Set by PluginLoader after
|
// ── C++-only host state, never exposed to Python. Set by the loader at materialization. ──
|
||||||
// plugin capture so trampoline calls can scope filesystem enforcement to this
|
//
|
||||||
// plugin. This is PluginDescriptor::plugin_key, the canonical runtime id.
|
// 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); }
|
void set_audit_plugin_key(std::string key) { m_audit_plugin_key = std::move(key); }
|
||||||
const std::string& audit_plugin_key() const { return m_audit_plugin_key; }
|
const std::string& audit_plugin_key() const { return m_audit_plugin_key; }
|
||||||
|
|
||||||
|
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:
|
private:
|
||||||
|
std::string m_name;
|
||||||
|
PluginCapabilityType m_type = PluginCapabilityType::Unknown;
|
||||||
|
std::atomic<bool> m_enabled{true};
|
||||||
std::string m_audit_plugin_key;
|
std::string m_audit_plugin_key;
|
||||||
|
|
||||||
|
mutable std::atomic<int> m_refs{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -105,7 +105,10 @@ struct GilSafeCallable
|
|||||||
{
|
{
|
||||||
if (fn) {
|
if (fn) {
|
||||||
PythonGILState gil;
|
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 nullptr;
|
||||||
return [holder](const json& data) {
|
return [holder](const json& data) {
|
||||||
PythonGILState gil;
|
PythonGILState gil;
|
||||||
|
if (!gil)
|
||||||
|
return;
|
||||||
try {
|
try {
|
||||||
holder->fn(json_to_py(data));
|
holder->fn(json_to_py(data));
|
||||||
} catch (py::error_already_set& e) {
|
} 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) {
|
if (close_holder) {
|
||||||
on_close = [close_holder]() {
|
on_close = [close_holder]() {
|
||||||
PythonGILState gil;
|
PythonGILState gil;
|
||||||
|
if (!gil)
|
||||||
|
return;
|
||||||
try {
|
try {
|
||||||
close_holder->fn();
|
close_holder->fn();
|
||||||
} catch (py::error_already_set& e) {
|
} 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)
|
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)
|
bool progress_pulse(int id, const std::string& message)
|
||||||
@@ -462,7 +469,10 @@ void PluginHostUi::RegisterBindings(pybind11::module_& host)
|
|||||||
auto ui = host.def_submodule(
|
auto ui = host.def_submodule(
|
||||||
"ui",
|
"ui",
|
||||||
"Host UI: native dialogs and interactive HTML windows. Calls run on the main/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",
|
ui.def("message", &ui_message, py::arg("text"), py::arg("title") = "OrcaSlicer", py::arg("buttons") = "ok",
|
||||||
py::arg("icon") = "info",
|
py::arg("icon") = "info",
|
||||||
|
|||||||
@@ -9,15 +9,18 @@ namespace Slic3r {
|
|||||||
// Binds the `orca.host.ui` submodule: native message boxes, progress dialogs,
|
// 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
|
// 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.
|
// (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
|
class PluginHostUi
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static void RegisterBindings(pybind11::module_& host);
|
static void RegisterBindings(pybind11::module_& host);
|
||||||
|
|
||||||
// Lifecycle hook: close and tear down every UI window owned by a plugin.
|
// Lifecycle hook: close and tear down every UI window owned by a plugin. PluginManager invokes
|
||||||
// Registered via PluginLoader::subscribe_on_unload_callback so UI windows
|
// this after plugin teardown and also for bulk unload during application shutdown.
|
||||||
// are destroyed on plugin unload/reload and at app shutdown (before the
|
|
||||||
// Python interpreter is finalized). Matches PluginLifecycleCompleteFn.
|
|
||||||
static void close_windows_for_plugin(const std::string& plugin_key);
|
static void close_windows_for_plugin(const std::string& plugin_key);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "../../PyPluginTrampoline.hpp"
|
#include "../../PyPluginTrampoline.hpp"
|
||||||
|
|
||||||
#include "IPrinterAgent.hpp"
|
#include "IPrinterAgent.hpp"
|
||||||
|
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
class PyPrinterAgentPluginCapabilityTrampoline : public PyPluginCommonTrampoline<PrinterAgentPluginCapability>
|
class PyPrinterAgentPluginCapabilityTrampoline : public PyPluginCommonTrampoline<PrinterAgentPluginCapability>
|
||||||
@@ -216,7 +217,10 @@ public:
|
|||||||
int request_bind_ticket(std::string* ticket) override
|
int request_bind_ticket(std::string* ticket) override
|
||||||
{
|
{
|
||||||
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
|
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::function override =
|
||||||
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
|
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
|
||||||
if (!override)
|
if (!override)
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ struct SlicingPipelineContext {
|
|||||||
class SlicingPipelinePluginCapability : public PluginCapabilityInterface {
|
class SlicingPipelinePluginCapability : public PluginCapabilityInterface {
|
||||||
public:
|
public:
|
||||||
PluginCapabilityType get_type() const override { return PluginCapabilityType::SlicingPipeline; }
|
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;
|
virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0;
|
||||||
static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes);
|
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
|
add_executable(${_TEST_NAME}_tests
|
||||||
${_TEST_NAME}_tests_main.cpp
|
${_TEST_NAME}_tests_main.cpp
|
||||||
test_plugin_host_api.cpp
|
test_plugin_host_api.cpp
|
||||||
test_plugin_capability_identifier.cpp
|
|
||||||
test_plugin_install.cpp
|
test_plugin_install.cpp
|
||||||
|
test_plugin_lifecycle.cpp
|
||||||
test_slicing_pipeline_bindings.cpp
|
test_slicing_pipeline_bindings.cpp
|
||||||
test_plugin_sort.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.
|
// 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");
|
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;
|
PluginDescriptor descriptor;
|
||||||
// is_cloud_plugin() -> true; cloud_uuid() -> the traversal string.
|
// is_cloud_plugin() -> true; cloud_uuid() -> the traversal string.
|
||||||
descriptor.cloud = CloudPluginState{"../../escape", true, false, false, false};
|
descriptor.cloud = CloudPluginState{"../../escape", true, false, false, false};
|
||||||
|
|
||||||
std::string error;
|
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);
|
REQUIRE_FALSE(installed);
|
||||||
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("valid identifier"));
|
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.
|
// 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");
|
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;
|
PluginDescriptor descriptor;
|
||||||
std::string error;
|
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);
|
REQUIRE_FALSE(installed);
|
||||||
CHECK_THAT(error, Catch::Matchers::ContainsSubstring("PEP 723"));
|
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";
|
"print('ok')\n";
|
||||||
const fs::path py = write_py_file(data_dir_guard.dir / "src", "good.py", contents);
|
const fs::path py = write_py_file(data_dir_guard.dir / "src", "good.py", contents);
|
||||||
|
|
||||||
PluginLoader loader; // non-cloud
|
|
||||||
|
|
||||||
PluginDescriptor descriptor;
|
PluginDescriptor descriptor;
|
||||||
std::string error;
|
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).
|
// Positive control: a complete side-loaded .py must still install (guards against over-rejection).
|
||||||
REQUIRE(installed);
|
REQUIRE(installed);
|
||||||
@@ -168,10 +165,9 @@ TEST_CASE("install_plugin parses [tool.orcaslicer.plugin.settings] into descript
|
|||||||
"print('ok')\n";
|
"print('ok')\n";
|
||||||
const fs::path py = write_py_file(data_dir_guard.dir / "src", "settings.py", contents);
|
const fs::path py = write_py_file(data_dir_guard.dir / "src", "settings.py", contents);
|
||||||
|
|
||||||
PluginLoader loader; // non-cloud
|
|
||||||
PluginDescriptor descriptor;
|
PluginDescriptor descriptor;
|
||||||
std::string error;
|
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);
|
REQUIRE(installed);
|
||||||
CHECK(error.empty());
|
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