diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 619723de0b..4bc1889c07 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -618,6 +618,8 @@ set(SLIC3R_GUI_SOURCES plugin/PluginLoader.cpp plugin/PluginLoader.hpp plugin/PluginDescriptor.hpp + plugin/PluginHooks.cpp + plugin/PluginHooks.hpp plugin/PluginManager.cpp plugin/PluginManager.hpp plugin/PluginAuditManager.cpp diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index b593563ad4..04bb7b5fe1 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -81,7 +81,6 @@ #include "slic3r/plugin/PluginManager.hpp" #include "slic3r/plugin/host/PluginHostUi.hpp" #include "slic3r/plugin/PythonInterpreter.hpp" -#include "slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp" #include "GUI.hpp" #include "GUI_Utils.hpp" @@ -2701,6 +2700,54 @@ std::string get_system_info() return out.str(); } +// wx/app-level plugin wiring, kept in one place: subscriptions to plugin +// loader events that drive GUI policy (plugins dialog refresh, network-agent +// registration, plate revalidation). The libslic3r dispatch hooks are NOT +// wired here -- PluginManager::initialize() installs those via +// plugin_hooks::install(). +void GUI_App::init_plugin_gui_wiring() +{ + PluginManager& plugin_mgr = PluginManager::instance(); + + auto refresh_plugins_dialog = [] { + if (!wxTheApp) + return; + + GUI_App* app = &GUI::wxGetApp(); + if (app->is_closing()) + return; + + app->CallAfter([app] { + if (!app->is_closing() && app->m_plugins_dlg) + app->m_plugins_dlg->update_plugin_dialog_ui(); + }); + }; + + plugin_mgr.get_loader().subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); + plugin_mgr.get_loader().subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); + plugin_mgr.get_loader().subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin); + plugin_mgr.get_loader().subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin); + plugin_mgr.get_loader().subscribe_on_capability_load_callback( + [refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) { + if (capability.type == PluginCapabilityType::PrinterConnection) + NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name); + refresh_plugins_dialog(); + // A newly loaded capability may satisfy a missing-plugin notification; re-validate the + // current plate (on the UI thread) so the notification clears once its plugin is available. + if (wxTheApp && !wxGetApp().is_closing()) + wxGetApp().CallAfter([]() { + if (Plater* plater = wxGetApp().plater()) + plater->revalidate_current_plate_if_plugins_missing(); + }); + }); + plugin_mgr.get_loader().subscribe_on_capability_unload_callback( + [refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) { + if (capability.type == PluginCapabilityType::PrinterConnection) + NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); + refresh_plugins_dialog(); + }); +} + bool GUI_App::on_init_inner() { wxLog::SetActiveTarget(new wxBoostLog()); @@ -3104,94 +3151,12 @@ bool GUI_App::on_init_inner() on_init_network(); // Initialize plugins after network then register on_load callbacks so once the plugin loads finish, it gets registered automatically. + // initialize() also installs the libslic3r hooks (capability resolver, + // slicing-pipeline dispatcher) via plugin_hooks::install() -- no + // per-capability wiring belongs here. PluginManager& plugin_mgr = PluginManager::instance(); plugin_mgr.initialize(); - ConfigBase::set_resolve_capability_fn([&plugin_mgr](const std::string& cap_name, const std::string& cap_type) { - auto plugin_cap = plugin_mgr.get_loader().try_get_plugin_capability_by_name_and_type(cap_name, plugin_capability_type_from_string(cap_type)); - if (!plugin_cap) - return std::string(); - - PluginDescriptor descriptor; - if (!plugin_mgr.get_catalog().try_get_plugin_descriptor(plugin_cap->plugin_key, descriptor)) - return std::string(); - - // Cloud plugins are resolved at runtime via the UUID in the middle field, so the first - // field keeps the friendly display name. Local plugins are looked up by plugin_key (the - // first field, with an empty UUID), so emit the plugin_key to keep them resolvable. - const std::string identity = descriptor.is_cloud_plugin() ? descriptor.name : descriptor.plugin_key; - return identity + ';' + descriptor.cloud_uuid() + ';' + cap_name; - }); - - // Orca: register the slicing-pipeline plugin dispatcher (mirrors set_resolve_capability_fn: the - // GUI/plugin layer supplies the Python bridge so libslic3r stays free of any plugin dependency). - // Print::process() fires this hook at each pipeline seam on the slicing worker thread; here we run - // the picker-selected SlicingPipeline capabilities. Per capability we acquire the GIL, honor - // cancellation, and convert a plugin failure into a (non-critical) SlicingError so it surfaces as a - // slicing-error notification rather than the fatal-crash dialog. - Slic3r::Print::set_slicing_pipeline_hook_fn( - [](Slic3r::Print& print, const Slic3r::PrintObject* object, Slic3r::SlicingPipelineStepPlugin step) { - const auto* caps = print.config().option("slicing_pipeline_plugin"); - // `plugins` is a dynamic-only manifest key (not a static PrintConfig member), so it - // must be read from the full/dynamic config -- reading it off print.config() (the - // static PrintConfig) always yields nullptr and skips every capability. Mirrors the - // post-process path (PostProcessor.cpp, via BackgroundSlicingProcess::full_print_config()). - const auto* plugs = print.full_print_config().option("plugins"); - if (caps == nullptr || caps->values.empty()) - return; - - Slic3r::execute_capabilities_from_refs( - *caps, plugs, Slic3r::PluginCapabilityType::SlicingPipeline, - [&](std::shared_ptr cap, const Slic3r::PluginCapabilityRef& ref) { - Slic3r::ExecutionResult r; - try { - // GIL is acquired per capability (not once for the whole dispatch) so it - // is released between capabilities. - PythonGILState gil; - // throw_if_canceled() is protected on PrintBase; canceled() is the public - // equivalent check (same cancel flag), so honor cancellation via it. - if (print.canceled()) - throw Slic3r::CanceledException(); - Slic3r::SlicingPipelineContext ctx; - ctx.orca_version = SoftFever_VERSION; - ctx.step = step; - ctx.print = &print; - ctx.object = object; - // hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params - // (same plugin_key the capability was resolved by, so it always matches). - const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid; - ctx.params = Slic3r::PluginManager::instance().get_loader().get_plugin_settings(plugin_key); - r = cap->execute(ctx); - } catch (const Slic3r::CanceledException&) { - throw; // cancellation must reach process(), never become a slicing error - } catch (const std::exception& ex) { - // A Python raise reaches here as pybind11::error_already_set; surface it as a - // (non-critical) slicing error instead of a crash. - throw Slic3r::SlicingError(std::string("Slicing pipeline plugin '") + - ref.capability_name + "' error: " + ex.what()); - } - if (r.status == Slic3r::PluginResult::FatalError) - throw Slic3r::SlicingError(std::string("Slicing pipeline plugin '") + - ref.capability_name + "' error: " + r.message); - // log a non-empty success/skipped message instead of dropping it. This is - // log-only by design: every pipeline hook fires AFTER set_done() (see Print.cpp), - // so the Print-level m_step_active is -1 here. Calling active_step_add_warning() - // would then index m_state[-1] (out-of-bounds; the guarding assert is compiled - // out in Release), so it must NOT be called from a pipeline hook. - if (!r.message.empty()) { - static const char* const kStepNames[] = { - "posSlice", "posPerimeters", "posEstimateCurledExtrusions", "posPrepareInfill", "posInfill", - "posIroning", "posContouring", "posSupportMaterial", "posDetectOverhangsForLift", - "posSimplifyPath", "psWipeTower", "psSkirtBrim", "psGCodePostProcess" - }; // order must match Slic3r::SlicingPipelineStepPlugin - const char* step_name = static_cast(step) < sizeof(kStepNames) / sizeof(kStepNames[0]) - ? kStepNames[static_cast(step)] : "Unknown"; - BOOST_LOG_TRIVIAL(info) << "Slicing pipeline plugin '" << ref.capability_name - << "' [" << step_name << "]: " << r.message; - } - }); - }); - // Set cloud plugin directory from previous session so cloud-installed // plugins are discovered even before the network agent is ready. const std::string preset_folder = app_config->get("preset_folder"); @@ -3202,43 +3167,7 @@ bool GUI_App::on_init_inner() plugin_mgr.discover_plugins(false, true); - auto refresh_plugins_dialog = [] { - if (!wxTheApp) - return; - - GUI_App* app = &GUI::wxGetApp(); - if (app->is_closing()) - return; - - app->CallAfter([app] { - if (!app->is_closing() && app->m_plugins_dlg) - app->m_plugins_dlg->update_plugin_dialog_ui(); - }); - }; - - plugin_mgr.get_loader().subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); - plugin_mgr.get_loader().subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); }); - plugin_mgr.get_loader().subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin); - plugin_mgr.get_loader().subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin); - plugin_mgr.get_loader().subscribe_on_capability_load_callback( - [refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) { - if (capability.type == PluginCapabilityType::PrinterConnection) - NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name); - refresh_plugins_dialog(); - // A newly loaded capability may satisfy a missing-plugin notification; re-validate the - // current plate (on the UI thread) so the notification clears once its plugin is available. - if (wxTheApp && !wxGetApp().is_closing()) - wxGetApp().CallAfter([]() { - if (Plater* plater = wxGetApp().plater()) - plater->revalidate_current_plate_if_plugins_missing(); - }); - }); - plugin_mgr.get_loader().subscribe_on_capability_unload_callback( - [refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) { - if (capability.type == PluginCapabilityType::PrinterConnection) - NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name); - refresh_plugins_dialog(); - }); + init_plugin_gui_wiring(); for (const std::string& plugin_key : plugin_mgr.get_catalog().get_enabled_plugin_keys()) { if (!plugin_mgr.get_loader().is_plugin_loaded(plugin_key)) { diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 19aeef17b4..ede7743f75 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -772,6 +772,9 @@ private: bool on_init_network(bool try_backup = false); void init_networking_callbacks(); void init_app_config(); + // GUI-side subscriptions to plugin loader events (dialog refresh, + // network-agent registration, plate revalidation). + void init_plugin_gui_wiring(); void remove_old_networking_plugins(); void drain_pending_events(int timeout_ms); bool wait_for_network_idle(int timeout_ms); diff --git a/src/slic3r/plugin/PluginHooks.cpp b/src/slic3r/plugin/PluginHooks.cpp new file mode 100644 index 0000000000..428c79223e --- /dev/null +++ b/src/slic3r/plugin/PluginHooks.cpp @@ -0,0 +1,129 @@ +#include "PluginHooks.hpp" + +#include "PluginManager.hpp" +#include "PythonInterpreter.hpp" +#include "PythonPluginInterface.hpp" +#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp" + +#include "libslic3r/Config.hpp" +#include "libslic3r/Exception.hpp" +#include "libslic3r/Print.hpp" +#include "libslic3r_version.h" + +#include + +#include +#include + +namespace Slic3r::plugin_hooks { +namespace { + +// Manifest resolver: turns the bare capability name a preset stores into the full +// "name;uuid;capability" reference the dispatchers consume (see +// ConfigBase::collect_plugin_manifest / update_plugin_manifest). +void install_capability_resolver() +{ + ConfigBase::set_resolve_capability_fn([](const std::string& cap_name, const std::string& cap_type) { + PluginManager& plugin_mgr = PluginManager::instance(); + auto plugin_cap = plugin_mgr.get_loader().try_get_plugin_capability_by_name_and_type(cap_name, plugin_capability_type_from_string(cap_type)); + if (!plugin_cap) + return std::string(); + + PluginDescriptor descriptor; + if (!plugin_mgr.get_catalog().try_get_plugin_descriptor(plugin_cap->plugin_key, descriptor)) + return std::string(); + + // Cloud plugins are resolved at runtime via the UUID in the middle field, so the first + // field keeps the friendly display name. Local plugins are looked up by plugin_key (the + // first field, with an empty UUID), so emit the plugin_key to keep them resolvable. + const std::string identity = descriptor.is_cloud_plugin() ? descriptor.name : descriptor.plugin_key; + return identity + ';' + descriptor.cloud_uuid() + ';' + cap_name; + }); +} + +// Print::process() fires this hook at each pipeline seam on the slicing worker +// thread; here we run the picker-selected SlicingPipeline capabilities. Per +// capability we acquire the GIL, honor cancellation, and convert a plugin +// failure into a (non-critical) SlicingError so it surfaces as a slicing-error +// notification rather than the fatal-crash dialog. +void install_slicing_pipeline_hook() +{ + Print::set_slicing_pipeline_hook_fn( + [](Print& print, const PrintObject* object, SlicingPipelineStepPlugin step) { + const auto* caps = print.config().option("slicing_pipeline_plugin"); + // `plugins` is a dynamic-only manifest key (not a static PrintConfig member), so it + // must be read from the full/dynamic config -- reading it off print.config() (the + // static PrintConfig) always yields nullptr and skips every capability. Mirrors the + // post-process path (PostProcessor.cpp, via BackgroundSlicingProcess::full_print_config()). + const auto* plugs = print.full_print_config().option("plugins"); + if (caps == nullptr || caps->values.empty()) + return; + + execute_capabilities_from_refs( + *caps, plugs, PluginCapabilityType::SlicingPipeline, + [&](std::shared_ptr cap, const PluginCapabilityRef& ref) { + ExecutionResult r; + try { + // GIL is acquired per capability (not once for the whole dispatch) so it + // is released between capabilities. + PythonGILState gil; + // throw_if_canceled() is protected on PrintBase; canceled() is the public + // equivalent check (same cancel flag), so honor cancellation via it. + if (print.canceled()) + throw CanceledException(); + SlicingPipelineContext ctx; + ctx.orca_version = SoftFever_VERSION; + ctx.step = step; + ctx.print = &print; + ctx.object = object; + // hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params + // (same plugin_key the capability was resolved by, so it always matches). + const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid; + ctx.params = PluginManager::instance().get_loader().get_plugin_settings(plugin_key); + r = cap->execute(ctx); + } catch (const CanceledException&) { + throw; // cancellation must reach process(), never become a slicing error + } catch (const std::exception& ex) { + // A Python raise reaches here as pybind11::error_already_set; surface it as a + // (non-critical) slicing error instead of a crash. + throw SlicingError(std::string("Slicing pipeline plugin '") + + ref.capability_name + "' error: " + ex.what()); + } + if (r.status == PluginResult::FatalError) + throw SlicingError(std::string("Slicing pipeline plugin '") + + ref.capability_name + "' error: " + r.message); + // log a non-empty success/skipped message instead of dropping it. This is + // log-only by design: every pipeline hook fires AFTER set_done() (see Print.cpp), + // so the Print-level m_step_active is -1 here. Calling active_step_add_warning() + // would then index m_state[-1] (out-of-bounds; the guarding assert is compiled + // out in Release), so it must NOT be called from a pipeline hook. + if (!r.message.empty()) { + static const char* const kStepNames[] = { + "posSlice", "posPerimeters", "posEstimateCurledExtrusions", "posPrepareInfill", "posInfill", + "posIroning", "posContouring", "posSupportMaterial", "posDetectOverhangsForLift", + "posSimplifyPath", "psWipeTower", "psSkirtBrim", "psGCodePostProcess" + }; // order must match SlicingPipelineStepPlugin + const char* step_name = static_cast(step) < sizeof(kStepNames) / sizeof(kStepNames[0]) + ? kStepNames[static_cast(step)] : "Unknown"; + BOOST_LOG_TRIVIAL(info) << "Slicing pipeline plugin '" << ref.capability_name + << "' [" << step_name << "]: " << r.message; + } + }); + }); +} + +} // namespace + +void install() +{ + install_capability_resolver(); + install_slicing_pipeline_hook(); +} + +void uninstall() +{ + ConfigBase::set_resolve_capability_fn(nullptr); + Print::set_slicing_pipeline_hook_fn(nullptr); +} + +} // namespace Slic3r::plugin_hooks diff --git a/src/slic3r/plugin/PluginHooks.hpp b/src/slic3r/plugin/PluginHooks.hpp new file mode 100644 index 0000000000..bff276c5df --- /dev/null +++ b/src/slic3r/plugin/PluginHooks.hpp @@ -0,0 +1,21 @@ +#pragma once + +// The plugin layer's installers for the hooks libslic3r exposes. libslic3r +// stays free of any plugin/Python dependency: it exposes static setter seams +// (ConfigBase::set_resolve_capability_fn, Print::set_slicing_pipeline_hook_fn, +// ...) and this unit injects the dispatchers -- one file-local installer per +// hook, aggregated by install(). Capabilities dispatched from the GUI layer +// (e.g. PostProcessor.cpp) call execute_capabilities_from_refs at their own +// call site and need no hook here. + +namespace Slic3r::plugin_hooks { + +// Install every hook. Called once from PluginManager::initialize(). +void install(); + +// Reset every hook to null so none can enter Python after the interpreter +// finalizes. Called from PluginManager::shutdown(); callers must have stopped +// background slicing first (resetting a hook while process() runs is a race). +void uninstall(); + +} // namespace Slic3r::plugin_hooks diff --git a/src/slic3r/plugin/PluginManager.cpp b/src/slic3r/plugin/PluginManager.cpp index 7062dfda29..33b692c2df 100644 --- a/src/slic3r/plugin/PluginManager.cpp +++ b/src/slic3r/plugin/PluginManager.cpp @@ -5,6 +5,7 @@ #include "PythonPluginBridge.hpp" #include "PluginFsUtils.hpp" +#include "PluginHooks.hpp" #include "PythonFileUtils.hpp" #include @@ -120,6 +121,10 @@ bool PluginManager::initialize() m_initialized = true; + // Install the libslic3r hooks (capability resolver, slicing-pipeline + // dispatcher). Uninstalled in shutdown() before the interpreter finalizes. + plugin_hooks::install(); + // Persist auto-load / capability state to each plugin's .install_state.json sidecar. // On load: write enabled=true plus current capability flags. On unload: flip enabled=false. // The on-unload callback is skipped during shutdown (run_on_unload_callbacks is gated by @@ -154,6 +159,10 @@ void PluginManager::shutdown() BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": PluginManager shutdown enter"; + // Detach the libslic3r hooks first so nothing dispatches into Python while + // (or after) plugins unload. Callers stop background slicing before this. + plugin_hooks::uninstall(); + // Signal the loader to reject new plugin loads before we drain. m_loader.set_shutting_down();