#include "PluginLoader.hpp" #include "PluginCatalog.hpp" #include "PluginFsUtils.hpp" #include "PythonFileUtils.hpp" #include "PythonPluginBridge.hpp" #include "PythonInterpreter.hpp" #include "libslic3r/Utils.hpp" #include "slic3r/Utils/NetworkAgentFactory.hpp" #include namespace py = pybind11; #include #include #include #include #ifdef _WIN32 #include #endif #include #include #include #include #include #include #include #include #include #include "PluginAuditManager.hpp" namespace Slic3r { namespace { boost::filesystem::path canonical_or_absolute(const boost::filesystem::path& path) { namespace fs = boost::filesystem; boost::system::error_code ec; fs::path resolved = fs::weakly_canonical(path, ec); if (!ec && !resolved.empty()) return resolved; ec.clear(); resolved = fs::absolute(path, ec); if (!ec && !resolved.empty()) return resolved; return path; } std::string plugin_package_extension(const boost::filesystem::path& path) { std::string ext = path.extension().string(); boost::algorithm::to_lower(ext); return ext; } boost::filesystem::path local_plugin_root() { return boost::filesystem::path(data_dir()) / "orca_plugins"; } boost::filesystem::path local_plugin_install_dir(const boost::filesystem::path& source_path) { // Folder name follows the plugin filename and is the install/backup conflict unit; // plugin_key is the file stem of the same file. return local_plugin_root() / filesystem_safe_escape(source_path.filename().string()); } void assign_local_plugin_key(PluginDescriptor& plugin_descriptor, const boost::filesystem::path& entry_file) { plugin_descriptor.plugin_key = make_local_plugin_key(entry_file.stem().string()); } bool read_local_plugin_package_metadata(const boost::filesystem::path& source_path, PluginDescriptor& plugin_descriptor, std::string& error) { error.clear(); const std::string ext = plugin_package_extension(source_path); if (ext != ".py" && ext != ".whl") { error = "Plugin package must be a .py or .whl file, got: " + ext; return false; } bool ok = false; if (ext == ".whl") ok = read_wheel_plugin_metadata(source_path, plugin_descriptor, error); else ok = read_python_plugin_metadata(source_path, plugin_descriptor, error); if (!ok) return false; plugin_descriptor.cloud = std::nullopt; return true; } } // namespace bool PluginLoader::is_idle_and_empty() const { std::lock_guard lock(m_mutex); return m_plugin_load_in_progress.empty() && m_plugins.empty(); } bool PluginLoader::is_plugin_loaded(const std::string& plugin_key) const { std::lock_guard lock(m_mutex); return m_plugins.find(plugin_key) != m_plugins.end(); } bool PluginLoader::is_plugin_load_in_progress(const std::string& plugin_key) const { std::lock_guard lock(m_mutex); return m_plugin_load_in_progress.count(plugin_key) > 0; } void PluginLoader::wait_for_all_plugin_loads() const { std::unique_lock lock(m_mutex); m_plugin_load_cv.wait(lock, [this]() { return m_plugin_load_in_progress.empty(); }); } bool PluginLoader::wait_for_all_plugin_loads(std::chrono::milliseconds timeout) const { std::unique_lock lock(m_mutex); return m_plugin_load_cv.wait_for(lock, timeout, [this]() { return m_plugin_load_in_progress.empty(); }); } bool PluginLoader::wait_for_plugin_load(const std::string& plugin_key, std::chrono::milliseconds timeout, std::string& error) const { std::unique_lock lock(m_mutex); auto done = [this, &plugin_key]() { return m_plugin_load_in_progress.count(plugin_key) == 0; }; if (timeout == std::chrono::milliseconds::max()) { m_plugin_load_cv.wait(lock, done); } else if (!m_plugin_load_cv.wait_for(lock, timeout, done)) { error = "Plugin load is still in progress"; return false; } auto it = m_plugin_load_errors.find(plugin_key); if (it != m_plugin_load_errors.end()) { error = it->second; return false; } return true; } std::vector PluginLoader::get_all_loaded_plugin_descriptors() const { std::lock_guard lock(m_mutex); std::vector result; result.reserve(m_plugins.size()); for (const auto& [key, loaded] : m_plugins) { (void) key; result.push_back(loaded.descriptor); } return result; } std::shared_ptr PluginLoader::try_get_plugin_capability_by_name_and_type(const std::string& capability_name, PluginCapabilityType type) const { std::lock_guard lock(m_mutex); auto find_by_name = [&capability_name](const PluginCapabilityMap& capabilities) -> std::shared_ptr { for (const auto& [id, capability] : capabilities) { if (capability && capability->name == capability_name) return capability; } return nullptr; }; if (type != PluginCapabilityType::Unknown) { const auto type_it = m_plugin_capabilities.find(type); return type_it == m_plugin_capabilities.end() ? nullptr : find_by_name(type_it->second); } for (const auto& [capability_type, capabilities] : m_plugin_capabilities) { (void) capability_type; if (auto capability = find_by_name(capabilities)) return capability; } return nullptr; } std::vector> PluginLoader::get_plugin_capabilities_by_type(const std::string& plugin_type) const { return get_plugin_capabilities_by_type(plugin_capability_type_from_string(plugin_type)); } std::vector> PluginLoader::get_plugin_capabilities_by_type(PluginCapabilityType type) const { std::lock_guard lock(m_mutex); auto type_it = m_plugin_capabilities.find(type); if (type_it == m_plugin_capabilities.end()) return {}; std::vector> result; result.reserve(type_it->second.size()); for (const auto& [id, capability] : type_it->second) { (void) id; result.push_back(capability); } std::sort(result.begin(), result.end(), [](const auto& lhs, const auto& rhs) { if (!lhs || !rhs) return static_cast(lhs); return lhs->name == rhs->name ? lhs->plugin_key < rhs->plugin_key : lhs->name < rhs->name; }); return result; } std::vector> PluginLoader::get_plugin_capabilities_by_type(const std::string& plugin_key, PluginCapabilityType type) const { std::lock_guard lock(m_mutex); std::vector> result; if (m_plugin_capabilities.find(type) == m_plugin_capabilities.end()) return result; for (auto& [key, val] : m_plugin_capabilities.at(type)) { if (val->plugin_key != plugin_key) continue; result.push_back(val); } return result; } std::shared_ptr PluginLoader::get_plugin_capability_by_name( const std::string& plugin_key, PluginCapabilityType type, const std::string& name) const { return get_plugin_capability_by_name(PluginCapabilityIdentifier{type, name, plugin_key}); } std::shared_ptr PluginLoader::get_plugin_capability_by_name(const PluginCapabilityIdentifier& identifier) const { std::lock_guard lock(m_mutex); auto type_it = m_plugin_capabilities.find(identifier.type); if (type_it == m_plugin_capabilities.end()) return nullptr; if (type_it->second.find(identifier) != type_it->second.end()) { return type_it->second.at(identifier); } return nullptr; } std::vector> PluginLoader::get_loaded_plugin_capabilities(const std::string& plugin_key) const { std::lock_guard lock(m_mutex); const auto plugin_it = m_plugins.find(plugin_key); if (plugin_it == m_plugins.end()) return {}; const LoadedPlugin& loaded = plugin_it->second; std::vector> result; result.reserve(loaded.capabilities.size()); for (const PluginCapabilityIdentifier& id : loaded.capabilities) { auto type_it = m_plugin_capabilities.find(id.type); if (type_it == m_plugin_capabilities.end()) continue; auto cap_it = type_it->second.find(id); if (cap_it != type_it->second.end()) result.push_back(cap_it->second); } return result; } void PluginLoader::write_loaded_plugin_install_state(const std::string& plugin_key) { PluginDescriptor descriptor; std::vector> caps; bool found = false; { std::lock_guard lock(m_mutex); auto it = m_plugins.find(plugin_key); if (it == m_plugins.end()) return; descriptor = it->second.descriptor; for (const PluginCapabilityIdentifier& id : it->second.capabilities) { auto type_it = m_plugin_capabilities.find(id.type); if (type_it == m_plugin_capabilities.end()) continue; auto cap_it = type_it->second.find(id); if (cap_it != type_it->second.end() && cap_it->second) caps.emplace_back(cap_it->second->name, cap_it->second->enabled.load()); } found = true; } if (!found || descriptor.plugin_root.empty()) return; write_install_state(boost::filesystem::path(descriptor.plugin_root), descriptor, /*enabled=*/true, caps); } std::vector> PluginLoader::extract_plugin_capabilities_locked(const LoadedPlugin& plugin) { std::vector> result; result.reserve(plugin.capabilities.size()); for (const PluginCapabilityIdentifier& id : plugin.capabilities) { auto type_it = m_plugin_capabilities.find(id.type); if (type_it == m_plugin_capabilities.end()) continue; auto node = type_it->second.extract(id); if (!node.empty()) result.push_back(std::move(node.mapped())); if (type_it->second.empty()) m_plugin_capabilities.erase(type_it); } return result; } void PluginLoader::teardown_capabilities(std::vector>& capabilities, std::size_t lifecycle_count) const { if (capabilities.empty()) return; if (!PythonInterpreter::instance().is_initialized()) { capabilities.clear(); return; } PythonGILState gil; lifecycle_count = std::min(lifecycle_count, capabilities.size()); for (std::size_t index = 0; index < lifecycle_count; ++index) { const auto& capability = capabilities[index]; if (!capability || !capability->instance) continue; try { capability->instance->on_unload(); } catch (const std::exception& ex) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Plugin on_unload failed: " << ex.what(); } catch (...) { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Plugin on_unload failed"; } } capabilities.clear(); } std::string PluginLoader::get_plugin_load_error(const std::string& plugin_key) const { std::lock_guard lock(m_mutex); const auto it = m_plugin_load_errors.find(plugin_key); if (it != m_plugin_load_errors.end()) return it->second; return ""; } bool PluginLoader::cancel_plugin_load(const std::string& plugin_key) { bool cancelled = false; { std::lock_guard lock(m_mutex); cancelled = cancel_plugin_load_locked(plugin_key); } notify_plugin_load_state_changed(cancelled); return cancelled; } bool PluginLoader::cancel_plugin_unload(const std::string& plugin_key) { BOOST_LOG_TRIVIAL(debug) << "Plugin unload cancellation requested but unload is synchronous: " << plugin_key; return false; } bool PluginLoader::cancel_plugin_load_locked(const std::string& plugin_key) { const auto in_progress_it = m_plugin_load_in_progress.find(plugin_key); const bool cancelled = in_progress_it != m_plugin_load_in_progress.end(); if (cancelled) { m_plugin_load_in_progress.erase(in_progress_it); m_plugin_load_errors.erase(plugin_key); } return cancelled; } bool PluginLoader::is_plugin_load_cancelled_locked(const std::string& plugin_key) const { return m_plugin_load_in_progress.count(plugin_key) == 0; } void PluginLoader::notify_plugin_load_state_changed(bool changed) { if (changed) m_plugin_load_cv.notify_all(); } bool PluginLoader::install_packages(const std::vector& pkgs, std::string& error) const { if (pkgs.empty()) return true; const std::string uv_path = PythonInterpreter::bundled_uv_path(); if (uv_path.empty()) { error = "Bundled uv executable not found. Python package installation is unavailable."; return false; } const std::string python_executable = PythonInterpreter::bundled_python_executable(); if (python_executable.empty()) { error = "Bundled Python executable not found. Python package installation is unavailable."; return false; } const std::string target_dir = PythonInterpreter::shared_packages_dir(); namespace fs = boost::filesystem; boost::system::error_code ec; fs::create_directories(target_dir, ec); if (ec) { error = "Failed to create package target directory: " + ec.message(); return false; } std::vector args = {"pip", "install", "--python", python_executable, "--no-python-downloads", "--target", target_dir}; args.insert(args.end(), pkgs.begin(), pkgs.end()); BOOST_LOG_TRIVIAL(info) << "Installing Python packages via uv for " << python_executable << ": " << boost::algorithm::join(pkgs, ", "); try { namespace process = boost::process; process::ipstream std_err; process::child child(uv_path, process::args(args), #ifdef _WIN32 // uv.exe (and the python.exe it spawns) are console-subsystem programs. // OrcaSlicer is a GUI app with no console of its own, so without this flag // Windows allocates a fresh console window for the child that flashes on // screen during startup plugin loading. Matches ProcessRunner/MediaPlayCtrl. process::windows::create_no_window, #endif process::std_err > std_err); std::string err_output; std::thread stderr_reader([&std_err, &err_output]() { std::string line; while (std::getline(std_err, line)) { err_output += line + '\n'; } }); // Wait up to 120 seconds for package install to complete. // // NOTE: we poll child.running() instead of calling child.wait_for(). // On macOS (which lacks sigtimedwait) boost.process v1 implements the // timed waits with a sigwait()-based fallback that deadlocks inside a // multi-threaded process: SIGCHLD is delivered to an arbitrary thread // and consumed by an async handler, so the worker thread's sigwait() // never returns and the timeout never fires — the plugin load hangs // forever (the "Loading" status never clears). child.running() uses // waitpid(WNOHANG), which behaves correctly on every platform. // (boost.process v2, in Boost >= 1.86, implements timed waits via Asio // and is unaffected — revisit this when the bundled Boost is upgraded.) constexpr auto kInstallTimeout = std::chrono::seconds(120); const auto deadline = std::chrono::steady_clock::now() + kInstallTimeout; std::error_code run_ec; while (child.running(run_ec)) { if (run_ec) { stderr_reader.join(); error = "Failed to query uv process status: " + run_ec.message(); BOOST_LOG_TRIVIAL(error) << error; return false; } if (std::chrono::steady_clock::now() >= deadline) { std::error_code term_ec; child.terminate(term_ec); child.wait(term_ec); stderr_reader.join(); error = "uv pip install timed out after 120s"; BOOST_LOG_TRIVIAL(error) << error; return false; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } stderr_reader.join(); const int exit_code = child.exit_code(); if (exit_code != 0) { if (!err_output.empty()) error = "uv pip install failed (exit code " + std::to_string(exit_code) + "): " + err_output; else error = "uv pip install failed with exit code " + std::to_string(exit_code); BOOST_LOG_TRIVIAL(error) << error; return false; } BOOST_LOG_TRIVIAL(info) << "Python packages installed successfully"; return true; } catch (const std::exception& ex) { error = std::string("Failed to run uv: ") + ex.what(); BOOST_LOG_TRIVIAL(error) << error; return false; } } void PluginLoader::unload_all_plugins() { std::vector>>> removed; std::vector> orphaned_capabilities; { std::lock_guard lock(m_mutex); removed.reserve(m_plugins.size()); for (auto& [key, loaded] : m_plugins) { (void) key; auto capabilities = extract_plugin_capabilities_locked(loaded); removed.emplace_back(std::move(loaded), std::move(capabilities)); } m_plugins.clear(); // Preserve teardown safety even if a prior invariant violation left an // unreferenced registry entry behind. for (auto& [capability_type, capabilities] : m_plugin_capabilities) { (void) capability_type; for (auto& [id, capability] : capabilities) { (void) id; orphaned_capabilities.push_back(std::move(capability)); } } m_plugin_capabilities.clear(); } for (auto& [loaded, capabilities] : removed) { (void) loaded; teardown_capabilities(capabilities, capabilities.size()); } teardown_capabilities(orphaned_capabilities, orphaned_capabilities.size()); } bool PluginLoader::unload_plugin(const std::string& plugin_key, PluginCapabilityType type) { auto notify_unload_complete = [this, &plugin_key]() { if (!m_shutting_down.load(std::memory_order_acquire)) run_on_unload_callbacks(plugin_key); }; std::optional removed; std::vector> removed_capabilities; bool cancelled = false; { std::lock_guard lock(m_mutex); // Cancel in-progress load for this plugin so load_plugin_impl discards the result. cancelled = cancel_plugin_load_locked(plugin_key); auto map_it = m_plugins.find(plugin_key); if (map_it != m_plugins.end()) { removed_capabilities = extract_plugin_capabilities_locked(map_it->second); removed.emplace(std::move(map_it->second)); m_plugins.erase(map_it); } } if (!removed) { notify_plugin_load_state_changed(cancelled); notify_unload_complete(); return true; } std::vector teardown_types; teardown_types.reserve(removed->capabilities.size()); for (const PluginCapabilityIdentifier& id : removed->capabilities) teardown_types.push_back(id.type); if (teardown_types.empty()) teardown_types.push_back(type); teardown_capabilities(removed_capabilities, removed_capabilities.size()); removed.reset(); notify_plugin_load_state_changed(cancelled); // The .install_state.json sidecar is flipped to enabled=false by the on-unload // callback (skipped during shutdown), so the auto-load list survives app exit. BOOST_LOG_TRIVIAL(info) << "Unloaded plugin: " << plugin_key; std::unordered_set torn_down_types; for (const PluginCapabilityType cap_type : teardown_types) { if (!torn_down_types.insert(cap_type).second) continue; switch (cap_type) { case PluginCapabilityType::PostProcessing: break; case PluginCapabilityType::PrinterConnection: NetworkAgentFactory::deregister_python_plugin(plugin_key); break; default: break; } } notify_unload_complete(); return true; } bool PluginLoader::unload_plugin(const std::string& plugin_key) { PluginCapabilityType type = PluginCapabilityType::Unknown; bool found = false; bool cancelled = false; { std::lock_guard lock(m_mutex); cancelled = cancel_plugin_load_locked(plugin_key); const auto map_it = m_plugins.find(plugin_key); if (map_it != m_plugins.end()) { type = map_it->second.descriptor.primary_capability_type(); found = true; } } notify_plugin_load_state_changed(cancelled); if (found) return unload_plugin(plugin_key, type); if (!m_shutting_down.load(std::memory_order_acquire)) run_on_unload_callbacks(plugin_key); return true; } void PluginLoader::load_plugin(PluginCatalog& catalog, const std::string& plugin_key, bool skip_deps, std::vector capabilities_to_enable) { std::string plugin_id = plugin_key; PluginDescriptor resolved_descriptor; if (catalog.try_get_valid_plugin_descriptor(plugin_key, resolved_descriptor)) plugin_id = resolved_descriptor.plugin_key; bool already_loaded = false; bool load_in_progress = false; { std::lock_guard lock(m_mutex); already_loaded = m_plugins.count(plugin_id) > 0; if (m_plugin_load_in_progress.count(plugin_id)) load_in_progress = true; } if (already_loaded) { // The plugin is already loaded, but the caller may be asking us to enable capabilities that // are currently disabled (e.g. resolving an inactive-plugin reference). The load path below // is skipped for an already-loaded plugin, so honor the request here. Runs outside m_mutex // (released above); enable_capability takes the lock itself. for (const auto& cap : get_loaded_plugin_capabilities(plugin_id)) { if (!cap) continue; const bool enable_all = capabilities_to_enable.empty(); const bool enable_requested = std::find(capabilities_to_enable.begin(), capabilities_to_enable.end(), cap->name) != capabilities_to_enable.end(); if (enable_all || enable_requested) enable_capability(plugin_id, cap->name, cap->type); } run_on_load_callbacks(plugin_id); return; } if (load_in_progress) return; if (m_shutting_down.load(std::memory_order_acquire)) { BOOST_LOG_TRIVIAL(info) << "Plugin load rejected — shutting down: " << plugin_id; run_on_load_callbacks(plugin_id); return; } if (!catalog.has_valid_plugin_descriptor(plugin_id)) { PluginDescriptor invalid; std::string message; if (catalog.try_get_invalid_plugin_descriptor(plugin_id, invalid)) { message = "Plugin is invalid: " + plugin_id; if (invalid.has_error()) message += " - " + invalid.normalized_error(); } else { message = "Plugin not found: " + plugin_id; } { std::lock_guard lock(m_mutex); m_plugin_load_errors[plugin_id] = message; BOOST_LOG_TRIVIAL(error) << message; } if (!invalid.plugin_key.empty()) catalog.set_plugin_error(plugin_id, message); run_on_load_callbacks(plugin_id); return; } { std::lock_guard lock(m_mutex); m_plugin_load_in_progress.insert(plugin_id); m_plugin_load_errors.erase(plugin_id); } catalog.clear_plugin_error(plugin_id); std::thread([this, &catalog, plugin_id, skip_deps, capabilities_to_enable]() { auto fail_unexpected = [this, &catalog, &plugin_id](std::string message) { BOOST_LOG_TRIVIAL(error) << "[load_plugin] Unexpected worker failure plugin=" << plugin_id << " error=" << message; catalog.set_plugin_error(plugin_id, message); bool load_state_changed = false; { std::lock_guard lock(m_mutex); m_plugin_load_errors[plugin_id] = std::move(message); load_state_changed = m_plugin_load_in_progress.erase(plugin_id) > 0; } notify_plugin_load_state_changed(load_state_changed); }; try { load_plugin_impl(catalog, plugin_id, skip_deps, capabilities_to_enable); } catch (const std::exception& ex) { fail_unexpected(std::string("Unexpected plugin load failure: ") + ex.what()); } catch (...) { fail_unexpected("Unexpected plugin load failure"); } if (!m_shutting_down.load(std::memory_order_acquire)) run_on_load_callbacks(plugin_id); }).detach(); } void PluginLoader::enable_capability(const std::string& plugin_key, const std::string& capability_name, PluginCapabilityType type) { std::optional changed; { std::lock_guard lock(m_mutex); auto type_it = m_plugin_capabilities.find(type); if (type_it != m_plugin_capabilities.end()) { const PluginCapabilityIdentifier id{type, capability_name, plugin_key}; auto cap_it = type_it->second.find(id); if (cap_it != type_it->second.end() && cap_it->second && !cap_it->second->enabled) { cap_it->second->enabled = true; changed = id; } } } if (!changed) return; write_loaded_plugin_install_state(plugin_key); run_on_capability_load_callbacks(*changed); } void PluginLoader::disable_capability(const std::string& plugin_key, const std::string& capability_name, PluginCapabilityType type) { std::optional changed; { std::lock_guard lock(m_mutex); auto type_it = m_plugin_capabilities.find(type); if (type_it != m_plugin_capabilities.end()) { const PluginCapabilityIdentifier id{type, capability_name, plugin_key}; auto cap_it = type_it->second.find(id); if (cap_it != type_it->second.end() && cap_it->second && cap_it->second->enabled) { cap_it->second->enabled = false; changed = id; } } } if (!changed) return; write_loaded_plugin_install_state(plugin_key); run_on_capability_unload_callbacks(*changed); } void PluginLoader::load_plugin_impl(PluginCatalog& catalog, const std::string& plugin_key, bool skip_deps, std::vector capabilities_to_enable) { BOOST_LOG_TRIVIAL(info) << "[load_plugin_impl] START plugin=" << plugin_key << " thread=" << std::this_thread::get_id(); auto fail = [this, &catalog, &plugin_key](std::string message) { BOOST_LOG_TRIVIAL(error) << "[load_plugin_impl] FAIL plugin=" << plugin_key << " error=" << message; catalog.set_plugin_error(plugin_key, message); bool load_state_changed = false; { std::lock_guard lock(m_mutex); m_plugin_load_errors[plugin_key] = std::move(message); load_state_changed = m_plugin_load_in_progress.erase(plugin_key) > 0; BOOST_LOG_TRIVIAL(error) << m_plugin_load_errors[plugin_key]; } notify_plugin_load_state_changed(load_state_changed); }; PythonInterpreter& interpreter = PythonInterpreter::instance(); if (!interpreter.is_initialized()) { fail("Python interpreter not initialized: " + interpreter.last_error()); return; } PluginDescriptor descriptor; if (!catalog.try_get_plugin_descriptor(plugin_key, descriptor)) { fail("Plugin manifest not found: " + plugin_key); return; } if (!descriptor.is_metadata_valid()) { std::string error = "Plugin manifest is invalid: " + plugin_key; if (descriptor.has_error()) error += " - " + descriptor.normalized_error(); fail(std::move(error)); return; } // Serialize the entire load sequence — only one thread may run it at a time. This // prevents sys.path / sys.modules races when two plugins share the same entry-point // filename (e.g. plugin.py), and keeps the capability-registry check-then-commit below // free of an interleaved load (which could otherwise force a wasted on_load/on_unload // rollback). install_packages() has a 120s timeout so this cannot block indefinitely. // Held until the load completes; released by load_lock's destructor. static std::mutex load_serializer; std::lock_guard load_lock(load_serializer); if (!skip_deps) { std::string pkg_install_error; if (!install_packages(descriptor.dependencies, pkg_install_error)) { fail("Failed to install plugin dependencies: " + pkg_install_error); return; } } PythonPluginBridge& bridge = PythonPluginBridge::instance(); bridge.begin_plugin_capture(descriptor.entry_path); // Extract any .whl dependency files in the plugin directory. namespace fs = boost::filesystem; { fs::path entry_path(descriptor.entry_path); fs::path plugin_dir = entry_path.has_extension() ? entry_path.parent_path() : entry_path; if (fs::exists(plugin_dir) && fs::is_directory(plugin_dir)) { PythonInterpreter& interp = PythonInterpreter::instance(); for (fs::directory_iterator it(plugin_dir); it != fs::directory_iterator(); ++it) { if (!fs::is_regular_file(it->status()) || it->path().extension() != ".whl") continue; // Skip the entry file itself - it's loaded by its own path below. if (it->path() == fs::path(descriptor.entry_path)) continue; std::string dep_error; fs::path dep_dir = plugin_dir / "__whl_extracted__" / it->path().stem().string(); if (!fs::exists(dep_dir)) { if (!extract_zip_to_directory(it->path(), dep_dir, dep_error)) { fail("Failed to extract plugin .whl dependency " + it->path().string() + ": " + dep_error); bridge.cancel_plugin_capture(descriptor.entry_path); return; } } std::string syspath_error; if (!interp.add_sys_path(dep_dir.string(), syspath_error)) { fail("Failed to add .whl dependency to sys.path: " + syspath_error); bridge.cancel_plugin_capture(descriptor.entry_path); return; } } } } std::string load_error; PyObject* module = nullptr; { PythonInterpreter& interp = PythonInterpreter::instance(); if (!descriptor.entry_package.empty()) { fs::path entry_path(descriptor.entry_path); if (entry_path.extension() == ".whl") { module = interp.load_module_from_whl(descriptor.entry_path, descriptor.entry_package, load_error); } else { module = interp.load_module_from_directory(descriptor.entry_path, descriptor.entry_package, load_error); } } else { module = interp.load_module_from_file(descriptor.entry_path, load_error); } } if (!module) { fail("Failed to load plugin module: " + load_error); bridge.cancel_plugin_capture(descriptor.entry_path); return; } LoadedPlugin loaded; loaded.module = module; std::string bridge_error; // finalize_plugin_capture runs the module's @orca.plugin package class register_capabilities() // (while g_active_plugin_key is set), then instantiates each registered capability and // caches its get_name(). Returns one entry per capability. auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, bridge_error); if (!bridge_error.empty()) { PythonGILState gil; capabilities_found.clear(); fail("Plugin registration failed: " + bridge_error); return; } if (capabilities_found.empty()) { fail("Plugin module did not register any capabilities"); return; } descriptor.clear_error(); loaded.capabilities.reserve(capabilities_found.size()); // Per-capability enabled state comes from the plugin's cached install-state sidecar. // Capabilities not listed default to enabled. Matched by name within the plugin. PluginInstallState install_state; const bool have_install_state = catalog.try_get_install_state(plugin_key, install_state); std::unordered_map> seen_capabilities; std::vector> capabilities; capabilities.reserve(capabilities_found.size()); std::vector capability_types; capability_types.reserve(capabilities_found.size()); std::string materialization_error; { PythonGILState gil; try { for (auto& cap : capabilities_found) { if (!cap.instance) { materialization_error = "Plugin capability instance is null"; break; } auto loaded_cap = std::make_shared(); loaded_cap->instance = std::move(cap.instance); loaded_cap->name = cap.name; loaded_cap->plugin_key = descriptor.plugin_key; loaded_cap->type = loaded_cap->instance->get_type(); const PluginCapabilityIdentifier capability_id{ loaded_cap->type, loaded_cap->name, loaded_cap->plugin_key}; bool cap_enabled = capabilities_to_enable.empty() ? true : std::find_if(capabilities_to_enable.begin(), capabilities_to_enable.end(), [cap](const std::string& val) { return val == cap.name; }) != capabilities_to_enable.end(); if (have_install_state) { for (const auto& [cap_name, cap_state] : install_state.capabilities) { if (cap_name == loaded_cap->name) { cap_enabled = cap_state; break; } } } loaded_cap->enabled = cap_enabled; if (!seen_capabilities[loaded_cap->type].insert(loaded_cap->name).second) { materialization_error = "Plugin declares duplicate capability '" + loaded_cap->name + "' for type " + plugin_capability_type_to_string(loaded_cap->type); break; } loaded_cap->instance->set_audit_plugin_key(descriptor.plugin_key); capability_types.push_back(loaded_cap->type); loaded.capabilities.push_back(capability_id); capabilities.emplace_back(std::move(loaded_cap)); } } catch (const std::exception& ex) { materialization_error = std::string("Plugin capability materialization failed: ") + ex.what(); } catch (...) { materialization_error = "Plugin capability materialization failed"; } if (!materialization_error.empty()) { capabilities.clear(); capabilities_found.clear(); } } if (!materialization_error.empty()) { fail(std::move(materialization_error)); return; } capabilities_found.clear(); descriptor.capability_types = capability_types; loaded.descriptor = descriptor; bool cancelled = false; std::string registry_error; { std::lock_guard lock(m_mutex); cancelled = is_plugin_load_cancelled_locked(plugin_key); if (!cancelled) { const auto package_it = m_plugins.find(descriptor.plugin_key); if (package_it != m_plugins.end()) registry_error = "Plugin package is already loaded: " + descriptor.plugin_key; } if (!cancelled && registry_error.empty()) { for (const auto& capability : capabilities) { auto type_it = m_plugin_capabilities.find(capability->type); const PluginCapabilityIdentifier cap_id{capability->type, capability->name, capability->plugin_key}; if (type_it != m_plugin_capabilities.end() && type_it->second.count(cap_id) > 0) { registry_error = "Capability collision for type " + plugin_capability_type_to_string(capability->type) + " and name '" + capability->name + "'"; break; } } } } if (cancelled || !registry_error.empty()) { teardown_capabilities(capabilities, 0); if (!registry_error.empty()) fail(std::move(registry_error)); return; } std::size_t lifecycle_count = 0; try { PythonGILState gil; for (const auto& capability : capabilities) { ++lifecycle_count; capability->instance->on_load(); } } catch (const std::exception& ex) { teardown_capabilities(capabilities, lifecycle_count); fail(std::string("Plugin on_load failed: ") + ex.what()); return; } catch (...) { teardown_capabilities(capabilities, lifecycle_count); fail("Plugin on_load failed"); return; } bool committed = false; cancelled = false; std::size_t inserted_capability_count = 0; { std::lock_guard lock(m_mutex); cancelled = is_plugin_load_cancelled_locked(plugin_key); if (!cancelled) { const auto package_it = m_plugins.find(descriptor.plugin_key); if (package_it != m_plugins.end()) registry_error = "Plugin package is already loaded: " + descriptor.plugin_key; } if (!cancelled && registry_error.empty()) { for (const auto& capability : capabilities) { auto type_it = m_plugin_capabilities.find(capability->type); const PluginCapabilityIdentifier cap_id{capability->type, capability->name, capability->plugin_key}; if (type_it != m_plugin_capabilities.end() && type_it->second.count(cap_id) > 0) { registry_error = "Capability collision for type " + plugin_capability_type_to_string(capability->type) + " and name '" + capability->name + "'"; break; } } } if (!cancelled && registry_error.empty()) { try { for (const auto& capability : capabilities) { auto [type_it, type_inserted] = m_plugin_capabilities.try_emplace(capability->type); (void) type_inserted; auto [capability_it, capability_inserted] = type_it->second.try_emplace( PluginCapabilityIdentifier{capability->type, capability->name, capability->plugin_key}, capability); (void) capability_it; if (!capability_inserted) { registry_error = "Capability collision for type " + plugin_capability_type_to_string(capability->type) + " and name '" + capability->name + "'"; break; } ++inserted_capability_count; } if (registry_error.empty()) { auto [plugin_it, plugin_inserted] = m_plugins.try_emplace(descriptor.plugin_key, std::move(loaded)); (void) plugin_it; if (!plugin_inserted) registry_error = "Plugin package is already loaded: " + descriptor.plugin_key; else committed = true; } } catch (const std::exception& ex) { registry_error = std::string("Failed to register plugin capabilities: ") + ex.what(); } catch (...) { registry_error = "Failed to register plugin capabilities"; } } if (!committed) { for (std::size_t index = 0; index < inserted_capability_count; ++index) { const auto& capability = capabilities[index]; auto type_it = m_plugin_capabilities.find(capability->type); if (type_it == m_plugin_capabilities.end()) continue; type_it->second.erase( PluginCapabilityIdentifier{capability->type, capability->name, capability->plugin_key}); if (type_it->second.empty()) m_plugin_capabilities.erase(type_it); } for (auto type_it = m_plugin_capabilities.begin(); type_it != m_plugin_capabilities.end();) { if (type_it->second.empty()) type_it = m_plugin_capabilities.erase(type_it); else ++type_it; } } } if (!committed) { teardown_capabilities(capabilities, lifecycle_count); if (!cancelled) fail(std::move(registry_error)); return; } catalog.clear_plugin_error(plugin_key); bool load_state_changed = false; // The enabled plugin is persisted to its .install_state.json sidecar by the on-load // callback (subscribe_on_load_callback → write_loaded_plugin_install_state). { std::lock_guard lock(m_mutex); // unload_plugin() may cancel the load after the plugin was materialized // but before this worker reaches the registry/bookkeeping update. if (is_plugin_load_cancelled_locked(plugin_key)) return; load_state_changed = m_plugin_load_in_progress.erase(plugin_key) > 0; m_plugin_load_errors.erase(plugin_key); } notify_plugin_load_state_changed(load_state_changed); BOOST_LOG_TRIVIAL(info) << "[load_plugin_impl] SUCCESS plugin=" << plugin_key << " thread=" << std::this_thread::get_id(); } bool PluginLoader::inspect_local_plugin_package(const boost::filesystem::path& filepath, PluginDescriptor& plugin_descriptor, bool& existing_installation, std::string& error) const { namespace fs = boost::filesystem; error.clear(); plugin_descriptor = PluginDescriptor{}; existing_installation = false; auto fail = [&error](const std::string& message) { error = "Plugin package inspection failed: " + message; return false; }; boost::system::error_code ec; const fs::path source_path = canonical_or_absolute(filepath); if (!fs::exists(source_path, ec) || !fs::is_regular_file(source_path, ec)) return fail("Plugin package is not a file: " + filepath.string()); std::string metadata_error; if (!read_local_plugin_package_metadata(source_path, plugin_descriptor, metadata_error)) return fail(metadata_error); const fs::path final_dir = local_plugin_install_dir(source_path); assign_local_plugin_key(plugin_descriptor, source_path); plugin_descriptor.plugin_root = final_dir.string(); ec.clear(); const fs::file_status final_status = fs::status(final_dir, ec); if (ec && final_status.type() != fs::file_not_found) return fail("Failed to check existing plugin " + final_dir.string() + ": " + ec.message()); existing_installation = final_status.type() != fs::file_not_found && final_status.type() != fs::status_error; return true; } bool PluginLoader::install_plugin(const boost::filesystem::path& filepath, std::string& error) { PluginDescriptor descriptor{}; return install_plugin(filepath, descriptor, error); } bool PluginLoader::install_plugin(const boost::filesystem::path& filepath, PluginDescriptor& plugin_descriptor, std::string& error) { namespace fs = boost::filesystem; const std::string cloud_uuid = plugin_descriptor.cloud_uuid(); const bool is_cloud_install = !cloud_uuid.empty(); fs::path backup_dir; bool backup_created = false; auto fail = [&error](const std::string& message) { error = "Plugin installation failed: " + message; return false; }; boost::system::error_code ec; const fs::path source_path = canonical_or_absolute(filepath); if (!fs::exists(source_path, ec) || !fs::is_regular_file(source_path, ec)) return fail("Plugin package is not a file: " + filepath.string()); const std::string ext = plugin_package_extension(source_path); if (ext != ".py" && ext != ".whl") return fail("Plugin package must be a .py or .whl file, got: " + ext); if (is_cloud_install && cloud_uuid.empty()) return fail("Cloud plugin descriptor is missing UUID"); // The cloud UUID is concatenated into the install path (final_dir = plugin_root / cloud_uuid). // It comes verbatim from the server's catalog JSON, so reject anything that could escape the // per-user plugin root (traversal, leading dots, separators) before touching the filesystem. if (is_cloud_install && !is_valid_plugin_id(cloud_uuid)) return fail("Cloud plugin UUID is not a valid identifier: " + cloud_uuid); const fs::path plugin_root = is_cloud_install && !m_cloud_user_id.empty() ? fs::path(get_cloud_plugin_dir(m_cloud_user_id)) : local_plugin_root(); fs::create_directories(plugin_root, ec); if (ec) return fail("Failed to create plugin directory " + plugin_root.string() + ": " + ec.message()); std::string meta_error; if (is_cloud_install) { PluginDescriptor package_metadata; if (!read_local_plugin_package_metadata(source_path, package_metadata, meta_error)) return fail(meta_error); apply_plugin_metadata_fallbacks(plugin_descriptor, package_metadata); plugin_descriptor.dependencies = std::move(package_metadata.dependencies); if (plugin_descriptor.plugin_key.empty()) plugin_descriptor.plugin_key = cloud_uuid; if (!plugin_descriptor.cloud.has_value()) plugin_descriptor.cloud = CloudPluginState{cloud_uuid, true, false, false, false}; else plugin_descriptor.cloud->installed = true; } else { if (!read_local_plugin_package_metadata(source_path, plugin_descriptor, meta_error)) return fail(meta_error); // A side-loaded .py with no (or incomplete) PEP 723 block parses as success but has no // usable identity: an empty name collides in local_plugin_install_dir() (every nameless // plugin maps to orca_plugins/"path"), so // it installs but never loads. Wheels are exempt: read_wheel_plugin_metadata() already // requires a Name and may legitimately leave type == Unknown. if (ext == ".py" && plugin_descriptor.name.empty()) return fail("Side-loaded .py plugin is missing required PEP 723 metadata: 'name' is required"); } const fs::path final_dir = is_cloud_install ? plugin_root / cloud_uuid : local_plugin_install_dir(source_path); const fs::path dest_file = final_dir / source_path.filename(); // Local key is the plugin file stem; cloud key is the cloud UUID. if (is_cloud_install) { plugin_descriptor.plugin_key = cloud_uuid; } else { assign_local_plugin_key(plugin_descriptor, source_path); } // Backup existing installation if present. if (fs::exists(final_dir, ec)) { backup_dir = plugin_root / (final_dir.filename().string() + ".backup-" + fs::unique_path("%%%%-%%%%-%%%%").string()); fs::rename(final_dir, backup_dir, ec); if (ec) return fail("Failed to backup existing plugin " + final_dir.string() + ": " + ec.message()); backup_created = true; } // Create the plugin directory and copy the file. fs::create_directories(final_dir, ec); if (ec) { if (backup_created) { boost::system::error_code restore_ec; fs::rename(backup_dir, final_dir, restore_ec); } return fail("Failed to create plugin directory " + final_dir.string() + ": " + ec.message()); } fs::copy_file(source_path, dest_file, ec); if (ec) { if (backup_created) { boost::system::error_code restore_ec; fs::remove_all(final_dir, restore_ec); fs::rename(backup_dir, final_dir, restore_ec); } return fail("Failed to copy plugin file to " + dest_file.string() + ": " + ec.message()); } // Update entry_path to the installed location. plugin_descriptor.plugin_root = final_dir.string(); plugin_descriptor.entry_path = dest_file.string(); plugin_descriptor.set_metadata_valid(true); plugin_descriptor.clear_error(); // Write install state sidecar before removing the backup. { PluginDescriptor installed_entry = plugin_descriptor; if (is_cloud_install) installed_entry.cloud = CloudPluginState{cloud_uuid, true, false, false, plugin_descriptor.cloud->is_mine}; if (!write_install_state(final_dir, installed_entry)) { // Roll back: remove new files and restore backup. boost::system::error_code ec; fs::remove_all(final_dir, ec); if (backup_created) { fs::rename(backup_dir, final_dir, ec); } return fail("Failed to write plugin install state: " + (final_dir / INSTALL_STATE_FILE).string()); } } if (backup_created) { boost::system::error_code remove_ec; fs::remove_all(backup_dir, remove_ec); if (remove_ec) BOOST_LOG_TRIVIAL(warning) << "Failed to remove plugin backup " << backup_dir.string() << ": " << remove_ec.message(); } BOOST_LOG_TRIVIAL(info) << "Installed plugin " << plugin_descriptor.name << " to " << final_dir.string(); if (is_cloud_install) { boost::system::error_code remove_ec; boost::filesystem::remove(source_path, remove_ec); } return true; } void PluginLoader::clear_loaded_plugin_cloud_state(const std::string& plugin_key) { std::lock_guard lock(m_mutex); const auto it = m_plugins.find(plugin_key); if (it != m_plugins.end()) it->second.descriptor.cloud.reset(); } void PluginLoader::update_loaded_plugin_key(const std::string& old_key, const std::string& new_key) { std::vector unloaded_capability_ids; std::vector loaded_capability_ids; { std::lock_guard lock(m_mutex); auto it = m_plugins.find(old_key); if (it == m_plugins.end()) return; const auto collision = m_plugins.find(new_key); if (collision != m_plugins.end() && collision != it) { BOOST_LOG_TRIVIAL(warning) << "Cannot update loaded plugin key to existing package key: " << new_key; return; } auto node = m_plugins.extract(it); const std::string original_map_key = node.key(); node.key() = new_key; auto insertion = m_plugins.insert(std::move(node)); if (!insertion.inserted) { insertion.node.key() = original_map_key; m_plugins.insert(std::move(insertion.node)); BOOST_LOG_TRIVIAL(warning) << "Cannot update loaded plugin key to existing package key: " << new_key; return; } LoadedPlugin& loaded = insertion.position->second; loaded.descriptor.plugin_key = new_key; loaded.descriptor.cloud.reset(); for (PluginCapabilityIdentifier& id : loaded.capabilities) { const PluginCapabilityIdentifier old_id = id; const PluginCapabilityIdentifier new_id{id.type, id.name, new_key}; auto type_it = m_plugin_capabilities.find(id.type); if (type_it != m_plugin_capabilities.end()) { auto capability_node = type_it->second.extract(id); if (!capability_node.empty() && capability_node.mapped()) { const bool enabled = capability_node.mapped()->enabled; capability_node.mapped()->plugin_key = new_key; if (capability_node.mapped()->instance) capability_node.mapped()->instance->set_audit_plugin_key(new_key); capability_node.key() = new_id; type_it->second.insert(std::move(capability_node)); if (enabled) { unloaded_capability_ids.push_back(old_id); loaded_capability_ids.push_back(new_id); } } } id = new_id; } } // Subscribers may key their own state by plugin_key. Publish the identity transition // after the registry update and outside m_mutex so they can safely query the loader. for (const PluginCapabilityIdentifier& id : unloaded_capability_ids) run_on_capability_unload_callbacks(id); for (const PluginCapabilityIdentifier& id : loaded_capability_ids) run_on_capability_load_callbacks(id); } void PluginLoader::unload_cloud_plugins() { std::vector> plugins_to_unload; { std::lock_guard lock(m_mutex); for (auto& [key, loaded] : m_plugins) { if (loaded.descriptor.is_cloud_plugin()) plugins_to_unload.emplace_back(loaded.descriptor.primary_capability_type(), loaded.descriptor.plugin_key); } } // Release m_mutex before unloading: unload_plugin() re-acquires it, and runs // plugin teardown + unload callbacks that can re-enter the loader. for (auto& [type, key] : plugins_to_unload) { unload_plugin(key, type); } } void PluginLoader::run_on_load_callbacks(const std::string& plugin_key) { for (auto& fn : m_callbacks[CallbackType::Load]) { try { fn(plugin_key); } catch (const std::exception& ex) { BOOST_LOG_TRIVIAL(error) << "Plugin load completion callback failed for " << plugin_key << ": " << ex.what(); } catch (...) { BOOST_LOG_TRIVIAL(error) << "Plugin load completion callback failed for " << plugin_key; } } } void PluginLoader::run_on_unload_callbacks(const std::string& plugin_key) { for (auto& fn : m_callbacks[CallbackType::Unload]) { try { fn(plugin_key); } catch (const std::exception& ex) { BOOST_LOG_TRIVIAL(error) << "Plugin unload completion callback failed for " << plugin_key << ": " << ex.what(); } catch (...) { BOOST_LOG_TRIVIAL(error) << "Plugin unload completion callback failed for " << plugin_key << ": unknown error"; } } } void PluginLoader::run_on_capability_load_callbacks(const PluginCapabilityIdentifier& id) { for (auto& fn : m_capability_callbacks[CallbackType::Load]) { try { fn(id); } catch (const std::exception& ex) { BOOST_LOG_TRIVIAL(error) << "Plugin capability load callback failed for " << id.plugin_key << "/" << id.name << ": " << ex.what(); } catch (...) { BOOST_LOG_TRIVIAL(error) << "Plugin capability load callback failed for " << id.plugin_key << "/" << id.name << ": unknown error"; } } } void PluginLoader::run_on_capability_unload_callbacks(const PluginCapabilityIdentifier& id) { for (auto& fn : m_capability_callbacks[CallbackType::Unload]) { try { fn(id); } catch (const std::exception& ex) { BOOST_LOG_TRIVIAL(error) << "Plugin capability unload callback failed for " << id.plugin_key << "/" << id.name << ": " << ex.what(); } catch (...) { BOOST_LOG_TRIVIAL(error) << "Plugin capability unload callback failed for " << id.plugin_key << "/" << id.name << ": unknown error"; } } } void PluginLoader::subscribe_on_load_callback(PluginLifecycleCompleteFn fn) { m_callbacks[CallbackType::Load].push_back(std::move(fn)); } void PluginLoader::subscribe_on_unload_callback(PluginLifecycleCompleteFn fn) { m_callbacks[CallbackType::Unload].push_back(std::move(fn)); } void PluginLoader::subscribe_on_capability_load_callback(CapabilityLifecycleFn fn) { m_capability_callbacks[CallbackType::Load].push_back(std::move(fn)); } void PluginLoader::subscribe_on_capability_unload_callback(CapabilityLifecycleFn fn) { m_capability_callbacks[CallbackType::Unload].push_back(std::move(fn)); } } // namespace Slic3r