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:
Ian Chua
2026-07-15 20:12:51 +08:00
committed by GitHub
parent 1551898846
commit 62afea225a
35 changed files with 4162 additions and 3373 deletions

View File

@@ -5,28 +5,110 @@
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <libslic3r/Config.hpp>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <pybind11/embed.h>
#include "CloudPluginService.hpp"
#include "PluginCatalog.hpp"
#include "PluginLoader.hpp"
#include "PluginFsUtils.hpp"
#include "PluginDescriptor.hpp"
#include "PluginLoader.hpp"
namespace Slic3r {
class OrcaCloudServiceAgent;
// Identity of a single capability, as published to lifecycle subscribers. Purely a message
// payload — capabilities are looked up by (plugin_key, name) linear scan, so unlike the registry
// key this replaces, it needs no hash and no equality.
struct PluginCapabilityId
{
PluginCapabilityType type = PluginCapabilityType::Unknown;
std::string name;
std::string plugin_key; // owning package
};
// One discovered plugin package: one .py/.whl file -> one descriptor + one Python module +
// N materialized capabilities.
//
// PluginManager holds every discovered plugin in a single std::vector<Plugin>, loaded or not;
// module == nullptr means the package was discovered but is not loaded. "What exists" and
// "what is live" are therefore the same vector, distinguished by a null check.
//
// A capability carries its own name, type and enable flag (PluginCapabilityInterface), cached by
// the loader at materialization, and its owning package via audit_plugin_key(). Capability state
// therefore lives exactly as long as the capability: a package that is not loaded has none. The
// only durable record is the .install_state.json sidecar, which the loader seeds enable flags from.
// The descriptor holds package state only.
struct Plugin
{
PluginDescriptor descriptor; // All plugin state and metadata lives here.
PyObject* module = nullptr; // Python module object, shared by all capabilities. nullptr => not loaded.
std::string module_name; // Root name used in sys.modules, including package submodules.
std::vector<std::string> plugin_sys_paths; // Paths added for this plugin load.
std::vector<std::string> plugin_modules; // Plugin-originated sys.modules entries.
// Materialized capability instances. Empty unless loaded.
std::vector<std::shared_ptr<PluginCapabilityInterface>> capabilities;
Plugin() = default;
Plugin(const Plugin&) = delete;
Plugin& operator=(const Plugin&) = delete;
// Move transfers module ownership; the moved-from package must not Py_DECREF it. Move
// assignment is required: the manager's vector is erased from and reordered.
Plugin(Plugin&& other) noexcept
: descriptor(std::move(other.descriptor)),
module(other.module),
module_name(std::move(other.module_name)),
plugin_sys_paths(std::move(other.plugin_sys_paths)),
plugin_modules(std::move(other.plugin_modules)),
capabilities(std::move(other.capabilities))
{
other.module = nullptr;
}
Plugin& operator=(Plugin&& other) noexcept
{
if (this != &other) {
release_module();
descriptor = std::move(other.descriptor);
module = other.module;
module_name = std::move(other.module_name);
plugin_sys_paths = std::move(other.plugin_sys_paths);
plugin_modules = std::move(other.plugin_modules);
capabilities = std::move(other.capabilities);
other.module = nullptr;
}
return *this;
}
~Plugin() { release_module(); }
bool is_loaded() const { return module != nullptr; }
// Removes the module namespace and plugin-owned sys.path entries before dropping the module
// reference. If the interpreter is already finalized, PythonInterpreter deliberately leaves
// the raw reference untouched because there is no safe way to DECREF it.
void release_module();
};
class PluginManager
{
public:
using PluginLifecycleCompleteFn = std::function<void(const std::string& /*plugin_key*/)>;
using CapabilityLifecycleFn = std::function<void(const PluginCapabilityId&)>;
static PluginManager& instance();
~PluginManager();
@@ -37,35 +119,86 @@ public:
// Stop discovery and unload Python plugin objects before Python finalizes.
void shutdown();
// Discover and scan plugins from standard directories (manifest-only, no Python loading).
// Runs on a worker thread when async=true.
// Reject new plugin loads. Called early in app teardown, before shutdown() drains.
void set_shutting_down();
void discover_plugins(bool async = false, bool clear = false);
// fetches plugins from the cloud
void fetch_plugins_from_cloud(std::vector<std::string>* out_not_found = nullptr,
std::vector<std::string>* out_unauthorized = nullptr);
// Download and install a cloud plugin from its download_url.
// Returns an error string on failure (empty URL, network down, etc.).
// On success returns an empty string.
bool download_and_install_cloud_plugin(const std::string& plugin_key, const std::string& version, std::string& error);
bool subscribe_and_install_cloud_plugin(const std::string& plugin_key, std::string& error);
// Manually trigger manifest-only rescan of plugins. Blocks until discovery is complete.
// Manually trigger a manifest-only rescan. Blocks until discovery is complete.
void rescan_plugins();
PluginCatalog& get_catalog() { return m_catalog; }
const PluginCatalog& get_catalog() const { return m_catalog; }
PluginLoader& get_loader() { return m_loader; }
const PluginLoader& get_loader() const { return m_loader; }
bool is_discovery_complete() const;
bool is_discovery_in_progress() const;
std::string get_discovery_error() const;
bool wait_for_discovery(std::chrono::milliseconds timeout, std::string& error) const;
std::vector<PluginDescriptor> get_plugin_descriptors(bool include_invalid = false) const;
// Packages that materialize at least one capability of this type. A package's capability types
// are only known once it has been loaded, so a never-loaded package never matches.
bool try_get_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& out) const;
// Same, but only for packages that are loadable (i.e. not an invalid package).
bool try_get_valid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& out) const;
// Packages whose .install_state.json marks them for auto-load.
std::vector<std::string> get_enabled_plugin_keys() const;
// The package owning a loaded capability, for the by-name dispatch path.
bool try_get_plugin_descriptor_for_capability(const std::string& capability_name,
PluginCapabilityType type,
PluginDescriptor& out) const;
std::vector<std::shared_ptr<PluginCapabilityInterface>> get_plugin_capabilities(
const std::string& plugin_key = "", // "" => all plugins
PluginCapabilityType type = PluginCapabilityType::Unknown, // Unknown => all types
bool only_enabled = true) const;
std::shared_ptr<PluginCapabilityInterface> get_plugin_capability(const std::string& plugin_key,
const std::string& capability_name,
PluginCapabilityType type = PluginCapabilityType::Unknown,
bool only_enabled = true) const;
std::shared_ptr<PluginCapabilityInterface> get_plugin_capability(const std::string& capability_name,
PluginCapabilityType type = PluginCapabilityType::Unknown,
bool only_enabled = true) const;
void load_plugin(const std::string& plugin_key, bool skip_deps = false, std::vector<std::string> capabilities_to_enable = {});
bool unload_plugin(const std::string& plugin_key);
void unload_all_plugins();
void unload_cloud_plugins();
bool is_plugin_loaded(const std::string& plugin_key) const;
bool is_plugin_load_in_progress(const std::string& plugin_key) const;
void wait_for_all_plugin_loads() const;
bool wait_for_all_plugin_loads(std::chrono::milliseconds timeout) const; // false on timeout
bool wait_for_plugin_load(const std::string& plugin_key, std::chrono::milliseconds timeout, std::string& error) const;
bool cancel_plugin_load(const std::string& plugin_key);
std::string get_plugin_load_error(const std::string& plugin_key) const;
void set_capability_enabled(const std::string& plugin_key, const std::string& capability_name, bool enabled);
// The plugin's [tool.orcaslicer.plugin.settings] table (empty if the plugin is unknown). This will be replaced once the config is merged in.
std::map<std::string, std::string> get_plugin_settings(const std::string& plugin_key) const;
// Sets the cloud user whose _subscribed/{user_id} directory is scanned and installed into.
void set_cloud_user(const std::string& user_id);
void subscribe_on_load_callback(PluginLifecycleCompleteFn fn);
void subscribe_on_unload_callback(PluginLifecycleCompleteFn fn);
void subscribe_on_capability_load_callback(CapabilityLifecycleFn fn);
void subscribe_on_capability_unload_callback(CapabilityLifecycleFn fn);
void set_cloud_agent(std::shared_ptr<OrcaCloudServiceAgent> agent) { m_cloud_service.set_cloud_agent(std::move(agent)); }
bool install_plugin(const boost::filesystem::path& filepath, std::string& error);
bool install_plugin(const boost::filesystem::path& filepath, PluginDescriptor& plugin_descriptor, std::string& error);
bool inspect_local_plugin_package(const boost::filesystem::path& filepath,
PluginDescriptor& plugin_descriptor,
bool& existing_installation,
std::string& error) const;
bool set_plugin_error(const std::string& plugin_key, std::string error);
bool clear_plugin_error(const std::string& plugin_key);
void fetch_plugins_from_cloud(std::vector<std::string>* out_not_found = nullptr, std::vector<std::string>* out_unauthorized = nullptr);
void update_cloud_catalog(const std::vector<PluginDescriptor>& cloud_list);
void clear_cloud_plugin_catalog();
bool download_and_install_cloud_plugin(const std::string& plugin_key, const std::string& version, std::string& error);
bool subscribe_and_install_cloud_plugin(const std::string& plugin_key, std::string& error);
// If the version is empty, take the latest version.
bool update_cloud_plugin(const std::string& plugin_key, std::string& error, std::string version = "");
@@ -75,32 +208,101 @@ public:
bool delete_mine_plugin_from_cloud(const std::string& plugin_key, std::string& error);
bool delete_mine_local_and_cloud_plugin(const std::string& plugin_key, std::string& error);
ExecutionResult run_script_capability(const std::string& plugin_key, const std::string& capability_name, std::string& error);
private:
PluginManager() = default;
PluginManager(const PluginManager&) = delete;
PluginManager& operator=(const PluginManager&) = delete;
enum class CallbackType { Load, Unload };
// Caller holds m_mutex. The returned pointer is invalidated by any push_back into m_plugins,
// so it must never escape the lock.
Plugin* find_plugin_locked(const std::string& plugin_key);
const Plugin* find_plugin_locked(const std::string& plugin_key) const;
void load_plugin_impl(const std::string& plugin_key, bool skip_deps, const std::vector<std::string>& capabilities_to_enable);
// Caller holds m_mutex. Non-empty return means the load must be abandoned.
std::string check_registry_locked(const std::string& plugin_key, const Plugin& candidate) const;
void run_discovery(bool async, bool clear);
void run_discovery_task(bool clear);
void merge_discovered_plugins(std::vector<PluginDescriptor> discovered, bool clear);
// Unload every loaded Plugin matching should_remove, then erase every matching entry (loaded or
// not). Unloading runs outside m_mutex (Python teardown and lifecycle callbacks can re-enter the
// manager), so the snapshot-unload cycle retries until a pass finds nothing left to unload; the
// final check and the erase itself run in one critical section so a concurrent load cannot make a
// live entry reach vector compaction and move-assignment. after_erase_locked, if given, runs right
// after the erase while m_mutex is still held.
void unload_and_erase_if(const std::function<bool(const Plugin&)>& should_remove,
const std::function<void()>& after_erase_locked = {});
bool cancel_plugin_load_locked(const std::string& plugin_key);
bool is_plugin_load_cancelled_locked(const std::string& plugin_key) const;
void notify_plugin_load_state_changed(bool changed);
void release_load_slot(const std::string& plugin_key);
void cancel_and_wait_for_capabilities(
const std::vector<std::shared_ptr<PluginCapabilityInterface>>& capabilities);
// Snapshot subscribers under m_mutex so they can be invoked without holding it.
std::vector<PluginLifecycleCompleteFn> copy_callbacks(CallbackType type) const;
std::vector<CapabilityLifecycleFn> copy_capability_callbacks(CallbackType type) const;
void run_on_load_callbacks(const std::string& plugin_key);
void run_on_unload_callbacks(const std::string& plugin_key);
void run_on_capability_load_callbacks(const PluginCapabilityId& id);
void run_on_capability_unload_callbacks(const PluginCapabilityId& id);
void clear_callbacks();
// Writes the sidecar for a loaded plugin (enabled=true plus the current per-capability flags).
void write_loaded_plugin_install_state(const std::string& plugin_key);
bool finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error);
bool delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error);
bool keep_installed_plugin_as_local(const PluginDescriptor& plugin_descriptor, std::string& error);
bool m_initialized = false;
CloudPluginService m_cloud_service;
PluginCatalog m_catalog;
PluginLoader m_loader;
// Leaf lock: code holding m_mutex must not call Python, acquire the GIL, invoke lifecycle
// callbacks, or re-enter the manager. Live plugin payloads are detached and torn down after
// releasing this lock.
mutable std::mutex m_mutex;
std::unordered_map<PluginCapabilityType, std::vector<std::function<void(const std::vector<PluginDescriptor>&)>>> m_loaded_plugin_changed_callbacks;
// Every discovered plugin, loaded or not. module == nullptr => not loaded.
std::vector<Plugin> m_plugins;
std::unordered_set<std::string> m_load_in_progress;
// Keys whose in-flight load has been cancelled. Cancellation does NOT remove the key from
// m_load_in_progress: the detached worker is still inside the loader touching Python, and it
// releases its own slot only once it has unwound. Dropping the key here would let
// wait_for_all_plugin_loads() return early, after which shutdown() unloads everything and
// GUI_App finalizes the interpreter — leaving the live worker to run Python against it.
std::unordered_set<std::string> m_cancelled;
std::map<std::string, std::string> m_load_errors;
mutable std::condition_variable m_load_cv;
std::map<CallbackType, std::vector<PluginLifecycleCompleteFn>> m_callbacks;
std::map<CallbackType, std::vector<CapabilityLifecycleFn>> m_capability_callbacks;
bool m_discovery_complete = false;
bool m_discovery_in_progress = false;
std::string m_discovery_error;
mutable std::condition_variable m_discovery_cv;
std::string m_cloud_user_id;
std::atomic<bool> m_shutting_down{false};
};
template <typename T>
// Resolve each configured capability reference to a loaded capability of type T and run `execute`.
template<typename T>
void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
const ConfigOptionStrings* plugins,
PluginCapabilityType type,
std::function<void(std::shared_ptr<T>, const PluginCapabilityRef&)> execute)
{
PluginManager& plugin_mgr = PluginManager::instance();
PluginManager& plugin_mgr = PluginManager::instance();
// Log prefix derived from the capability type so each capability family (Printer connection,
// Slicing Pipeline, ...) tags its dispatch diagnostics with its own display name.
@@ -108,7 +310,7 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
const bool has_any = std::any_of(capabilities.values.begin(), capabilities.values.end(),
[](const std::string& s) { return !s.empty(); });
if (has_any && !plugin_mgr.get_loader().wait_for_all_plugin_loads(std::chrono::seconds(10))) {
if (has_any && !plugin_mgr.wait_for_all_plugin_loads(std::chrono::seconds(10))) {
BOOST_LOG_TRIVIAL(warning) << tag << ": timed out waiting for plugin loads; unresolved capabilities will be skipped";
}
@@ -116,9 +318,6 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
if (capability.empty())
continue;
std::shared_ptr<LoadedPluginCapability> cap;
std::string cap_name, plugin_key;
std::optional<PluginCapabilityRef> ref;
if (plugins != nullptr) {
for (const std::string& plugin_ref : plugins->values) {
@@ -135,25 +334,26 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
continue;
}
cap_name = ref->capability_name;
plugin_key = ref->uuid.empty() ? ref->name : ref->uuid;
cap = plugin_mgr.get_loader().get_plugin_capability_by_name(plugin_key, type, cap_name);
const std::string cap_name = ref->capability_name;
const std::string plugin_key = ref->uuid.empty() ? ref->name : ref->uuid;
// only_enabled = false so that "not loaded" and "loaded but disabled" stay distinguishable
// and each keeps its own diagnostic.
auto cap = plugin_mgr.get_plugin_capability(plugin_key, cap_name, type, /*only_enabled=*/false);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << tag << ": no loaded capability '" << cap_name
<< "' for plugin '" << plugin_key << "'; skipping";
continue;
}
if (!cap->enabled) {
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name
<< "' for plugin '" << plugin_key << "' is disabled; skipping";
BOOST_LOG_TRIVIAL(warning) << tag << ": no loaded capability '" << cap_name << "' for plugin '" << plugin_key << "'; skipping";
continue;
}
auto plugin_capability = std::dynamic_pointer_cast<T>(cap->instance);
if (!cap->is_enabled()) {
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name << "' for plugin '" << plugin_key
<< "' is disabled; skipping";
continue;
}
auto plugin_capability = std::dynamic_pointer_cast<T>(cap);
if (!plugin_capability) {
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name
<< "' (plugin_key=" << cap->plugin_key
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name << "' (plugin_key=" << cap->audit_plugin_key()
<< ") is not a " << plugin_capability_type_to_string(type) << "; skipping";
continue;
}