Files
OrcaSlicer/src/slic3r/plugin/PluginManager.cpp
T
Ian Bassi e821665e4b Add orca-translations Claude skill and PO tool (#16436)
* Add orca-translations Claude skill and PO tool

Add a .claude skill that guides translation passes over the localization/i18n gettext catalogs. It covers the workflow: run gettext, settle new glossary terms, translate in batches, verify, and report.

Includes translation-rules.md, which condenses the placeholder, line-break and glossary rules plus the guidance on review notes.

Also adds scripts/po_tool.py (standard library only). It lists pending entries with glossary hints, validates and applies translations, adding '# AI Translated' and '# Needs human review' comments, and rewrites only the entries it changes. It can also check catalogs, look up existing translations, suggest glossary candidates and update the wiki glossary TSV.

* Let po_tool apply translations by entry id

pending now gives each entry a short id, so a translation file can name
entries by id instead of repeating the English msgid. apply accepts either.

A "review": "" item clears an earlier review note when overwriting an
entry, and check gives each msgfmt run its own temporary output file so
parallel checks do not collide.

* Fill all empty translations

Regenerate the template with run_gettext --full and translate every empty
entry in the 22 catalogs (7,089 entries) with the orca-translations skill,
following the localization glossary, including the new belt printer,
IDEX/IQEX and plugin terms. Every new entry is marked "# AI Translated";
122 also carry "# Needs human review" with what to check.

* Show entry ids in po_tool lookup and search translations

lookup now prints each entry's id, so an entry found there can be
rewritten with apply --overwrite without repeating the English, and
--msgstr searches the translations instead of the msgids, which finds
every entry that still uses a term being changed.

SKILL.md describes how to change an established term: check the
catalog's usage first, update the glossary cell, then rewrite only the
entries where the term has that meaning.

* Fix mistranslated terms and align catalogs with the glossary

Correct labels that the glossary had copied, and the entries that used
them:
- ca: Raft was rendered as the Brim term; it is now "Rai". Exhaust fan,
  Monotonic line, Unretract, Slice, Lateral Honeycomb and PA Tower
  labels fixed.
- cs: Lightning infill is "Blesk", not "Osvětlení" (illumination).
- sv: Retraction is "Retraktion", not "Reduktion"; Unretract,
  Lagerhöjd, Delkylningsfläkt, Utsugsfläkt, Aligned Rectilinear and the
  Preset label fixed.
- de: Brim is "Rand" and Skirt is "Umrandung", as most entries already
  had them.
- lt: brim entries used the Skirt word "apvadas"; they now use "kraštas".
- es: Sparse infill is "relleno de baja densidad", the catalog majority.
- eu: nozzle wipe is "garbiketa", not purge.
- th: the project plate is "เพลต", distinct from the bed.
- uk: Quarter Cubic, Latin "PA" in PA Tower, and Belt purge tower.
- pl: Cross Hatch, Tri-hexagon, Volumetric speed and Raft labels.
- zh_TW: Brim stays "Brim", as the existing labels have it.
- tr: "Prime Tower" read "Prime Minister Tower".
- ko: the plugin "Capabilities" label read "performance".
- Build Volume labels fixed in tr, de, cs and eu.

All 242 rewritten entries are marked "# AI Translated".

* Make more UI text translatable and fix pluralization and context

Audit the sources for text that translators could not translate
correctly and fix it.

Mark text that reached users in English only: IDEX/IQEX role names,
plugin permission prompts and errors, 3MF/preset loading errors, G-code
placeholder parser errors, calibration and device messages, notification
texts, file dialog filters, and the config validation messages.

Replace sentences built from translated fragments, such as the prime
tower warnings, "left"/"right" nozzle names, and "Filament " + number,
with single msgids that have placeholders, so translators can reorder
and inflect them.

Use plural forms for texts with a count ("%1% mixed filaments",
"%d objects", "%1% triangles") instead of "(s)" and mismatched verbs.
Fix the swapped singular/plural pair in "The following presets inherit
this preset".

Add translation contexts where one English word has several meanings
(Subscribed/Local, Standard, Pause, View, Step, Model, Camera, Flow,
Filter, Others, Prepare, the Design tools). Show "Size" instead of
"Build Volume" in the Cut tool, which displays the object size.

Fix code that compared translated text with English (the Edit G-code
dialog group, the plugin registration failure check), and add the
bundled web pages' missing string-table entries. Use the single
character for degrees Celsius, say "G-code", and describe the head
filament map without internal jargon.

Install the libslic3r translate callback before the presets load, so
their load errors are translated, and list the 28 source files that had
marked strings but were never extracted into localization/i18n/list.txt.

* Suggest earlier translations and list fuzzy entries in po_tool pending

pending now attaches the closest earlier translation of a similar
English text (from obsolete or translated entries), so a reworded or
split string keeps its wording, and it lists fuzzy entries by default,
since users never see them. A fuzzy entry also carries its stale
translation as previous_translation.

* Fix wording of the non-boolean operands error

The logical operation error said "operators" where it means the
operands the operation was applied to.

* Translate the newly translatable strings in all languages

Regenerate the template after the translatability changes and fill every
new or changed entry in the 22 catalogs (about 12,800 entries): the
strings that became translatable, the plural forms, the entries split by
context, and the previously fuzzy singular entries that became plurals.

Every new entry is marked "# AI Translated", and about 360 also carry a
"# Needs human review" note saying what to check. Terms follow the
localization glossary. Earlier translations of reworded strings were
reused where the meaning was unchanged. Existing entries were not
changed, and obsolete entries are kept.

* Document the source translatability audit in the orca-translations skill

Add a reference describing how strings reach translators, what to look
for (untranslated text, spliced sentences, missing plurals, ambiguous
words), what not to translate, where a context cannot be carried, and
what to do after the edits, plus a scanner that lists untranslated
user-facing string literals.

* Document how to write translatable strings in AGENTS.md

Tell developers to keep each message complete in one msgid: use
placeholders instead of concatenating translated fragments, plural forms
for counts, and contexts for ambiguous words, and to list new source
files in localization/i18n/list.txt. Add a matching code review item.

* Translate the strings added since the rebase

Regenerate the template against the current main and translate the 26
new entries per language: the Fix Model commands and messages, and the
texture displacement image adjustment panel. Each new entry is marked
"# AI Translated"; existing entries are unchanged.

* tidy
2026-10-13 10:53:57 -03:00

2140 lines
82 KiB
C++

#include "PluginManager.hpp"
#include <atomic>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <exception>
#include <functional>
#include "libslic3r/LifecycleEvents.hpp"
#include <memory>
#include <optional>
#include <pybind11/embed.h>
#include "PluginFsUtils.hpp"
#include "PluginHooks.hpp"
#include "PythonInterpreter.hpp"
#include "PythonPluginBridge.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "libslic3r/Semver.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/NotificationManager.hpp"
#include "slic3r/GUI/format.hpp"
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <chrono>
#include <mutex>
#include "slic3r/plugin/CloudPluginService.hpp"
#include <slic3r/plugin/PluginConfig.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp>
#include <stdexcept>
#include <thread>
#include <unordered_set>
#include <utility>
#include <vector>
#include <wx/app.h>
#include <wx/thread.h>
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
namespace Slic3r {
namespace {
// Sentinel error returned by the registry pre-check when the load was cancelled while it ran.
// A cancelled load records no error and fires no failure path — it just unwinds.
const char* const LOAD_CANCELLED = "__plugin_load_cancelled__";
} // namespace
void Plugin::release_module()
{
if (module == nullptr && plugin_sys_paths.empty() && plugin_modules.empty())
return;
PythonInterpreter::instance().unload_module(module, module_name, plugin_sys_paths, plugin_modules);
module = nullptr;
module_name.clear();
plugin_sys_paths.clear();
plugin_modules.clear();
}
PluginManager& PluginManager::instance()
{
// Touch the interpreter first so its static outlives this one: ~Plugin needs it alive to
// release module, sys.modules, and sys.path ownership safely.
PythonInterpreter::instance();
static PluginManager inst;
return inst;
}
PluginManager::~PluginManager() { shutdown(); }
bool PluginManager::initialize()
{
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_initialized)
return true;
m_shutting_down.store(false, std::memory_order_release);
}
// Initialize the Python interpreter eagerly on the main thread. CPython must be initialized
// from the main thread; calling Py_InitializeFromConfig from a background thread (e.g. the
// load_plugin worker) causes heap corruption in CPython internals.
PythonInterpreter& interpreter = PythonInterpreter::instance();
if (!interpreter.is_initialized() && !interpreter.initialize()) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Failed to initialize Python interpreter: " << interpreter.last_error();
return false;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_initialized)
return true;
m_initialized = true;
}
// Bring every capability's stored config into memory. Deliberately unconditional and
// independent of which plugins are installed: an entry outlives uninstall/unsubscribe, so a
// plugin that comes back later finds its settings intact. A missing or malformed file just
// leaves the store empty (see PluginConfig::load), never blocking startup.
m_config.load();
// 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 the current capability flags. On unload: flip enabled=false.
// The on-unload callback is skipped during shutdown, so app exit does not wipe the auto-load list.
subscribe_on_load_callback([this](const std::string& key) { write_loaded_plugin_install_state(key); });
subscribe_on_unload_callback([this](const std::string& key) {
if (m_shutting_down.load(std::memory_order_acquire))
return;
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(key, descriptor) || descriptor.plugin_root.empty())
return;
const boost::filesystem::path root(descriptor.plugin_root);
PluginInstallState state;
if (read_install_state(root, state)) {
state.enabled = false;
write_install_state(root, state);
}
});
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin manager initialized";
return true;
}
void PluginManager::set_shutting_down() { m_shutting_down.store(true, std::memory_order_release); }
void PluginManager::shutdown()
{
{
std::lock_guard<std::mutex> lock(m_mutex);
const bool idle_and_empty = m_load_in_progress.empty() && std::none_of(m_plugins.begin(), m_plugins.end(),
[](const Plugin& plugin) { return plugin.is_loaded(); });
if (!m_initialized && !m_discovery_in_progress && idle_and_empty)
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": PluginManager shutdown enter";
// Detach the libslic3r hooks first so nothing dispatches into Python while (or after) plugins
// unload. The lifecycle-event hook also drains callbacks already in progress before returning;
// the remaining hook seams retain their existing shutdown requirements.
plugin_hooks::uninstall();
// Reject new plugin loads before we drain.
set_shutting_down();
std::string wait_error;
if (!wait_for_discovery(std::chrono::milliseconds::max(), wait_error) && !wait_error.empty())
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Plugin discovery did not finish cleanly during shutdown: " << wait_error;
// Wait for any in-progress plugin loads. Cancelled loads keep their slot until their worker
// has actually unwound, so this cannot return while one is still executing Python.
wait_for_all_plugin_loads();
unload_all_plugins();
PythonPluginBridge::instance().clear_pending_captures();
// Every config write already goes to disk as it happens (store_capability_config), so this
// only catches an in-memory-only mutation. Note we flush rather than clear: unloading the
// plugins above must never discard their stored config.
if (m_config.dirty())
m_config.save();
// Drop the lifecycle subscriptions taken out during initialize(). Without this a second
// initialize() in the same process re-subscribes the same callbacks on top of the old ones,
// and every load would then write the install-state sidecar once per duplicate.
clear_callbacks();
{
std::lock_guard<std::mutex> lock(m_mutex);
m_initialized = false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": PluginManager shutdown exit";
}
// ── Registry helpers ────────────────────────────────────────────────────────────────────────
Plugin* PluginManager::find_plugin_locked(const std::string& plugin_key)
{
const auto it = std::find_if(m_plugins.begin(), m_plugins.end(),
[&plugin_key](const Plugin& plugin) { return plugin.descriptor.plugin_key == plugin_key; });
return it == m_plugins.end() ? nullptr : &*it;
}
const Plugin* PluginManager::find_plugin_locked(const std::string& plugin_key) const
{
const auto it = std::find_if(m_plugins.begin(), m_plugins.end(),
[&plugin_key](const Plugin& plugin) { return plugin.descriptor.plugin_key == plugin_key; });
return it == m_plugins.end() ? nullptr : &*it;
}
std::string PluginManager::check_registry_locked(const std::string& plugin_key, const Plugin& candidate) const
{
const Plugin* entry = find_plugin_locked(plugin_key);
if (entry != nullptr && entry->is_loaded())
return GUI::format(_u8L("Plugin package is already loaded: %1%"), plugin_key);
// A capability name must be unique per type across every loaded package.
for (const auto& capability : candidate.capabilities) {
if (!capability)
continue;
for (const Plugin& other : m_plugins) {
if (!other.is_loaded() || other.descriptor.plugin_key == plugin_key)
continue;
for (const auto& existing : other.capabilities) {
if (existing && existing->type() == capability->type() && existing->name() == capability->name())
// TRN %1% is the capability type (e.g. Script), %2% the capability name.
return GUI::format(_u8L("Capability collision for type %1% and name '%2%'"),
PluginConfig::capability_type_label(capability->type()), capability->name());
}
}
}
return {};
}
// ── Discovery ───────────────────────────────────────────────────────────────────────────────
void PluginManager::discover_plugins(bool async, bool clear)
{
if (!initialize())
return;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_discovery_in_progress) {
BOOST_LOG_TRIVIAL(debug) << "Plugin discovery already running";
return;
}
if (clear) {
for (Plugin& plugin : m_plugins)
plugin.descriptor.clear_error();
}
m_discovery_in_progress = true;
m_discovery_complete = false;
m_discovery_error.clear();
}
run_discovery(async, clear);
}
void PluginManager::rescan_plugins()
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Rescanning plugins...";
std::string wait_error;
wait_for_discovery(std::chrono::milliseconds::max(), wait_error);
discover_plugins(/*async=*/false, /*clear=*/true);
}
void PluginManager::run_discovery(bool async, bool clear)
{
auto task = [this, clear]() { run_discovery_task(clear); };
if (async)
std::thread(std::move(task)).detach();
else
task();
}
void PluginManager::run_discovery_task(bool clear)
{
std::string error;
try {
std::string cloud_user_id;
{
std::lock_guard<std::mutex> lock(m_mutex);
cloud_user_id = m_cloud_user_id;
}
const std::vector<std::string> dirs = get_plugin_directories(cloud_user_id);
std::vector<PluginDescriptor> discovered = discover_plugin_packages(dirs, error);
merge_discovered_plugins(std::move(discovered), clear);
} 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;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_discovery_error = std::move(error);
m_discovery_complete = true;
m_discovery_in_progress = false;
}
m_discovery_cv.notify_all();
}
void PluginManager::merge_discovered_plugins(std::vector<PluginDescriptor> discovered, bool clear)
{
std::vector<std::string> seen;
seen.reserve(discovered.size());
{
std::lock_guard<std::mutex> lock(m_mutex);
for (PluginDescriptor& descriptor : discovered) {
if (descriptor.plugin_key.empty())
continue;
if (std::find(seen.begin(), seen.end(), descriptor.plugin_key) != seen.end()) {
const std::string duplicate_error = GUI::format(_u8L("Duplicate plugin key discovered: %1%"), descriptor.plugin_key);
if (Plugin* existing = find_plugin_locked(descriptor.plugin_key)) {
existing->descriptor.set_error(duplicate_error);
BOOST_LOG_TRIVIAL(error) << duplicate_error << " (" << existing->descriptor.entry_path << ", "
<< descriptor.entry_path << ")";
} else {
BOOST_LOG_TRIVIAL(error) << duplicate_error;
}
continue;
}
seen.push_back(descriptor.plugin_key);
m_missing_plugin_keys.erase(descriptor.plugin_key);
Plugin* existing = find_plugin_locked(descriptor.plugin_key);
if (existing == nullptr) {
Plugin plugin;
plugin.descriptor = std::move(descriptor);
m_plugins.push_back(std::move(plugin));
continue;
}
// A manifest-only rescan has nothing to say about capabilities, so the live module and
// instances are left alone.
if (!descriptor.install_state_valid)
descriptor.enabled = existing->descriptor.enabled;
existing->descriptor = std::move(descriptor);
}
if (!clear)
return;
}
// A package can be temporarily absent while an external side-loader replaces it. Keep the
// descriptor and its persisted enable state until the user explicitly removes the missing
// entry, or a later scan rediscovers it. In particular, do not unload here: the unload callback
// would turn a transient filesystem gap into enabled=false in the sidecar.
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const Plugin& plugin : m_plugins) {
if (plugin.descriptor.has_local_package() &&
std::find(seen.begin(), seen.end(), plugin.descriptor.plugin_key) == seen.end())
m_missing_plugin_keys.insert(plugin.descriptor.plugin_key);
}
}
}
std::vector<PluginDescriptor> PluginManager::get_missing_plugin_descriptors() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<PluginDescriptor> result;
result.reserve(m_missing_plugin_keys.size());
for (const Plugin& plugin : m_plugins)
if (m_missing_plugin_keys.count(plugin.descriptor.plugin_key) != 0)
result.push_back(plugin.descriptor);
return result;
}
void PluginManager::remove_missing_plugins(const std::vector<std::string>& plugin_keys)
{
const std::unordered_set<std::string> requested(plugin_keys.begin(), plugin_keys.end());
// The predicate is evaluated only while m_mutex is held by unload_and_erase_if(). Checking the
// current missing set here prevents a package that reappeared between the dialog and removal
// from being erased.
unload_and_erase_if([this, &requested](const Plugin& plugin) {
return requested.count(plugin.descriptor.plugin_key) != 0 &&
m_missing_plugin_keys.count(plugin.descriptor.plugin_key) != 0;
});
std::lock_guard<std::mutex> lock(m_mutex);
for (const std::string& plugin_key : requested)
m_missing_plugin_keys.erase(plugin_key);
}
void PluginManager::unload_and_erase_if(const std::function<bool(const Plugin&)>& should_remove,
const std::function<void()>& after_erase_locked)
{
for (;;) {
std::vector<std::string> unload_keys;
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const Plugin& plugin : m_plugins) {
if (should_remove(plugin) && plugin.is_loaded())
unload_keys.push_back(plugin.descriptor.plugin_key);
}
}
if (unload_keys.empty()) {
std::lock_guard<std::mutex> lock(m_mutex);
// Keep the check and erase in one critical section so a concurrent load cannot make a
// live entry reach vector compaction and move-assignment.
bool loaded_entry_found = false;
for (const Plugin& plugin : m_plugins) {
if (should_remove(plugin) && plugin.is_loaded()) {
loaded_entry_found = true;
break;
}
}
if (loaded_entry_found)
continue;
m_plugins.erase(std::remove_if(m_plugins.begin(), m_plugins.end(), should_remove), m_plugins.end());
if (after_erase_locked)
after_erase_locked();
break;
}
for (const std::string& key : unload_keys)
unload_plugin(key);
}
}
bool PluginManager::is_discovery_complete() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_discovery_complete;
}
bool PluginManager::is_discovery_in_progress() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_discovery_in_progress;
}
std::string PluginManager::get_discovery_error() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_discovery_error;
}
bool PluginManager::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;
const auto done = [this]() { return !m_discovery_in_progress; };
if (timeout == std::chrono::milliseconds::max()) {
m_discovery_cv.wait(lock, done);
return true;
}
if (!m_discovery_cv.wait_for(lock, timeout, done)) {
error = "Plugin discovery is still running";
return false;
}
return true;
}
// ── Descriptors ─────────────────────────────────────────────────────────────────────────────
std::vector<PluginDescriptor> PluginManager::get_plugin_descriptors(bool include_invalid) const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<PluginDescriptor> result;
result.reserve(m_plugins.size());
for (const Plugin& plugin : m_plugins) {
if (!include_invalid && plugin.descriptor.is_invalid_package())
continue;
result.push_back(plugin.descriptor);
}
return result;
}
bool PluginManager::try_get_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& out) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const Plugin* plugin = find_plugin_locked(plugin_key);
if (plugin == nullptr)
return false;
out = plugin->descriptor;
return true;
}
bool PluginManager::try_get_valid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& out) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const Plugin* plugin = find_plugin_locked(plugin_key);
if (plugin == nullptr || plugin->descriptor.is_invalid_package())
return false;
out = plugin->descriptor;
return true;
}
std::vector<std::string> PluginManager::get_enabled_plugin_keys() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<std::string> keys;
for (const Plugin& plugin : m_plugins) {
if (plugin.descriptor.enabled && plugin.descriptor.has_local_package())
keys.push_back(plugin.descriptor.plugin_key);
}
return keys;
}
bool PluginManager::try_get_plugin_descriptor_for_capability(const std::string& capability_name,
PluginCapabilityType type,
PluginDescriptor& out) const
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const Plugin& plugin : m_plugins) {
for (const auto& capability : plugin.capabilities) {
if (!capability || capability->name() != capability_name)
continue;
if (type != PluginCapabilityType::Unknown && capability->type() != type)
continue;
out = plugin.descriptor;
return true;
}
}
return false;
}
std::string PluginManager::get_storage_dir(const std::string& plugin_key) const
{
namespace fs = boost::filesystem;
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor))
throw std::runtime_error("The current plugin is not registered");
const fs::path base_storage_dir = fs::path(get_orca_plugins_dir()) / PLUGIN_DATA_DIR;
if (!descriptor.is_cloud_plugin()) {
const fs::path local_storage_dir = base_storage_dir / plugin_key;
fs::create_directories(local_storage_dir);
return local_storage_dir.string();
}
auto agent = m_cloud_service.get_cloud_agent();
if (!agent)
throw std::runtime_error("Cloud plugin storage is unavailable before networking is initialized");
const std::string user_id = agent->get_user_id();
if (user_id.empty())
throw std::runtime_error("Cloud plugin storage is unavailable without a logged-in user");
if (!is_valid_plugin_id(plugin_key))
throw std::runtime_error("The current cloud plugin key is not a valid folder name");
const fs::path cloud_storage_dir = base_storage_dir / PLUGIN_SUBSCRIBED_DIR / user_id / plugin_key;
fs::create_directories(cloud_storage_dir);
return cloud_storage_dir.string();
}
// ── Capability instances ────────────────────────────────────────────────────────────────────
std::vector<std::shared_ptr<PluginCapabilityInterface>> PluginManager::get_plugin_capabilities(const std::string& plugin_key,
PluginCapabilityType type,
bool only_enabled) const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<std::shared_ptr<PluginCapabilityInterface>> result;
for (const Plugin& plugin : m_plugins) {
if (!plugin_key.empty() && plugin.descriptor.plugin_key != plugin_key)
continue;
for (const auto& capability : plugin.capabilities) {
if (!capability)
continue;
if (type != PluginCapabilityType::Unknown && capability->type() != type)
continue;
if (only_enabled && !capability->is_enabled())
continue;
result.push_back(capability);
}
}
return result;
}
std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(const PluginCapabilityId& id, bool only_enabled) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const Plugin* plugin = find_plugin_locked(id.plugin_key);
if (plugin == nullptr)
return nullptr;
for (const auto& capability : plugin->capabilities) {
if (!capability || capability->name() != id.name)
continue;
if (id.type != PluginCapabilityType::Unknown && capability->type() != id.type)
continue;
if (only_enabled && !capability->is_enabled())
continue;
return capability;
}
return nullptr;
}
std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(const std::string& capability_name,
PluginCapabilityType type,
bool only_enabled) const
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const Plugin& plugin : m_plugins) {
for (const auto& capability : plugin.capabilities) {
if (!capability || capability->name() != capability_name)
continue;
if (type != PluginCapabilityType::Unknown && capability->type() != type)
continue;
if (only_enabled && !capability->is_enabled())
continue;
return capability;
}
}
return nullptr;
}
bool PluginManager::get_install_state(const std::string& plugin_key, PluginInstallState& install_state)
{
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor)) {
return false;
}
if (!read_install_state(boost::filesystem::path(descriptor.plugin_root), install_state)) {
return false;
}
return true;
}
// ── Lifecycle ───────────────────────────────────────────────────────────────────────────────
bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const Plugin* plugin = find_plugin_locked(plugin_key);
return plugin != nullptr && plugin->is_loaded();
}
bool PluginManager::is_plugin_load_in_progress(const std::string& plugin_key) const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_load_in_progress.count(plugin_key) > 0;
}
void PluginManager::wait_for_all_plugin_loads() const
{
std::unique_lock<std::mutex> lock(m_mutex);
m_load_cv.wait(lock, [this]() { return m_load_in_progress.empty(); });
}
bool PluginManager::wait_for_all_plugin_loads(std::chrono::milliseconds timeout) const
{
std::unique_lock<std::mutex> lock(m_mutex);
return m_load_cv.wait_for(lock, timeout, [this]() { return m_load_in_progress.empty(); });
}
bool PluginManager::wait_for_plugin_load(const std::string& plugin_key, std::chrono::milliseconds timeout, std::string& error) const
{
std::unique_lock<std::mutex> lock(m_mutex);
const auto done = [this, &plugin_key]() { return m_load_in_progress.count(plugin_key) == 0; };
if (timeout == std::chrono::milliseconds::max()) {
m_load_cv.wait(lock, done);
} else if (!m_load_cv.wait_for(lock, timeout, done)) {
error = _u8L("Plugin load is still in progress");
return false;
}
const auto it = m_load_errors.find(plugin_key);
if (it != m_load_errors.end()) {
error = it->second;
return false;
}
return true;
}
std::string PluginManager::get_plugin_load_error(const std::string& plugin_key) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto it = m_load_errors.find(plugin_key);
return it == m_load_errors.end() ? std::string{} : it->second;
}
bool PluginManager::cancel_plugin_load(const std::string& plugin_key)
{
bool cancelled = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
cancelled = cancel_plugin_load_locked(plugin_key);
}
notify_plugin_load_state_changed(cancelled);
return cancelled;
}
bool PluginManager::cancel_plugin_load_locked(const std::string& plugin_key)
{
if (m_load_in_progress.count(plugin_key) == 0)
return false;
m_cancelled.insert(plugin_key);
m_load_errors.erase(plugin_key);
return true;
}
bool PluginManager::is_plugin_load_cancelled_locked(const std::string& plugin_key) const { return m_cancelled.count(plugin_key) > 0; }
void PluginManager::notify_plugin_load_state_changed(bool changed)
{
if (changed)
m_load_cv.notify_all();
}
void PluginManager::release_load_slot(const std::string& plugin_key)
{
bool changed = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
changed = m_load_in_progress.erase(plugin_key) > 0;
m_cancelled.erase(plugin_key);
}
notify_plugin_load_state_changed(changed);
}
void PluginManager::cancel_and_wait_for_capabilities(
const std::vector<std::shared_ptr<PluginCapabilityInterface>>& capabilities)
{
for (const auto& capability : capabilities) {
if (!capability || !capability->is_enabled())
continue;
try {
capability->on_cancelled();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "Plugin on_cancelled failed for '" << capability->audit_plugin_key() << "/"
<< capability->name() << "': " << ex.what();
} catch (...) {
BOOST_LOG_TRIVIAL(warning) << "Plugin on_cancelled failed for '" << capability->audit_plugin_key() << "/"
<< capability->name() << "'";
}
}
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (std::chrono::steady_clock::now() < deadline) {
const bool all_released = std::all_of(capabilities.begin(), capabilities.end(), [](const auto& capability) {
return !capability || capability->ref_count() == 0;
});
if (all_released)
return;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
for (const auto& capability : capabilities) {
if (!capability)
continue;
const int refs = capability->ref_count();
if (refs > 0) {
BOOST_LOG_TRIVIAL(warning) << "Forcing unload of plugin capability with " << refs
<< " active call(s): " << capability->audit_plugin_key() << "/" << capability->name();
}
}
}
void PluginManager::load_plugin(const std::string& plugin_key, bool skip_deps, std::vector<std::string> capabilities_to_enable)
{
std::string plugin_id = plugin_key;
bool exists = false;
bool invalid = false;
bool already_loaded = false;
bool load_in_progress = false;
bool load_slot_claimed = false;
bool shutting_down = false;
std::string invalid_error;
std::vector<std::string> loaded_capability_names;
{
std::lock_guard<std::mutex> lock(m_mutex);
const Plugin* plugin = find_plugin_locked(plugin_key);
if (plugin != nullptr) {
exists = true;
plugin_id = plugin->descriptor.plugin_key;
invalid = plugin->descriptor.is_invalid_package();
already_loaded = plugin->is_loaded();
invalid_error = plugin->descriptor.normalized_error();
if (already_loaded) {
for (const auto& capability : plugin->capabilities)
if (capability)
loaded_capability_names.push_back(capability->name());
}
}
load_in_progress = m_load_in_progress.count(plugin_id) > 0;
shutting_down = m_shutting_down.load(std::memory_order_acquire);
if (exists && !invalid && !already_loaded && !load_in_progress && !shutting_down) {
m_load_in_progress.insert(plugin_id);
m_load_errors.erase(plugin_id);
load_slot_claimed = true;
}
}
if (already_loaded) {
// An empty request means "restore the persisted capability state". Only an explicit
// capability request may change an already-loaded capability, e.g. when resolving an
// inactive-plugin reference. Runs outside m_mutex.
for (const std::string& capability_name : capabilities_to_enable) {
if (std::find(loaded_capability_names.begin(), loaded_capability_names.end(), capability_name) !=
loaded_capability_names.end())
set_capability_enabled({PluginCapabilityType::Unknown, capability_name, plugin_id}, true);
}
run_on_load_callbacks(plugin_id);
return;
}
if (load_in_progress)
return;
if (shutting_down) {
BOOST_LOG_TRIVIAL(info) << "Plugin load rejected — shutting down: " << plugin_id;
run_on_load_callbacks(plugin_id);
return;
}
if (!exists || invalid) {
std::string message;
if (invalid) {
message = GUI::format(_u8L("Plugin is invalid: %1%"), plugin_id);
if (!invalid_error.empty())
message += " - " + invalid_error;
} else {
message = GUI::format(_u8L("Plugin not found: %1%"), plugin_id);
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_load_errors[plugin_id] = message;
BOOST_LOG_TRIVIAL(error) << message;
}
if (invalid)
set_plugin_error(plugin_id, message);
run_on_load_callbacks(plugin_id);
return;
}
if (!load_slot_claimed)
return;
clear_plugin_error(plugin_id);
std::thread([this, plugin_id, skip_deps, capabilities_to_enable]() {
auto fail_unexpected = [this, &plugin_id](std::string message) {
BOOST_LOG_TRIVIAL(error) << "[load_plugin] Unexpected worker failure plugin=" << plugin_id << " error=" << message;
set_plugin_error(plugin_id, message);
{
std::lock_guard<std::mutex> lock(m_mutex);
m_load_errors[plugin_id] = std::move(message);
}
};
try {
load_plugin_impl(plugin_id, skip_deps, capabilities_to_enable);
} catch (const std::exception& ex) {
fail_unexpected(GUI::format(_u8L("Unexpected plugin load failure: %1%"), ex.what()));
} catch (...) {
fail_unexpected(_u8L("Unexpected plugin load failure"));
}
if (!m_shutting_down.load(std::memory_order_acquire))
run_on_load_callbacks(plugin_id);
// The worker owns its slot and releases it here, once it is done with Python for good —
// including a load that was cancelled mid-flight, which is what keeps
// wait_for_all_plugin_loads() blocking until the worker is really gone.
release_load_slot(plugin_id);
}).detach();
}
void PluginManager::load_plugin_impl(const std::string& plugin_key, bool skip_deps, const std::vector<std::string>& capabilities_to_enable)
{
auto fail = [this, &plugin_key](std::string message) {
BOOST_LOG_TRIVIAL(error) << "[load_plugin_impl] FAIL plugin=" << plugin_key << " error=" << message;
set_plugin_error(plugin_key, message);
{
std::lock_guard<std::mutex> lock(m_mutex);
m_load_errors[plugin_key] = std::move(message);
}
};
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor)) {
fail(GUI::format(_u8L("Plugin manifest not found: %1%"), plugin_key));
return;
}
// Reject a duplicate package or a capability-name collision BEFORE on_load() runs, so a
// rejected load costs no on_load/on_unload cycle. Also the point at which a cancellation that
// arrived while the module was importing is noticed.
auto registry_precheck = [this, &plugin_key](const Plugin& candidate) -> std::string {
std::lock_guard<std::mutex> lock(m_mutex);
if (is_plugin_load_cancelled_locked(plugin_key))
return LOAD_CANCELLED;
return check_registry_locked(plugin_key, candidate);
};
Plugin plugin;
std::string error;
bool registration_failed = false;
if (!plugin_loader::load(descriptor, skip_deps, capabilities_to_enable, registry_precheck, plugin, error, &registration_failed)) {
if (error == LOAD_CANCELLED)
return; // cancelled: nothing materialized survives, and no error is recorded
if (registration_failed)
mark_plugin_install_state_disabled(plugin_key);
fail(std::move(error));
return;
}
for (const auto& cap : plugin.capabilities) {
auto config = cap->get_default_config();
if (config.empty())
continue;
const PluginCapabilityId id = cap->identity();
if (m_config.has_config(id))
continue;
m_config.save_config({id, plugin.descriptor.installed_version, config});
}
bool committed = false;
bool cancelled = false;
std::string registry_error;
{
std::lock_guard<std::mutex> lock(m_mutex);
cancelled = is_plugin_load_cancelled_locked(plugin_key);
if (!cancelled) {
registry_error = check_registry_locked(plugin_key, plugin);
if (registry_error.empty()) {
Plugin* entry = find_plugin_locked(plugin_key);
if (entry == nullptr) {
registry_error = GUI::format(_u8L("Plugin manifest not found: %1%"), plugin_key);
} else {
// Move everything via Plugin's own move assignment, but the package's
// descriptor (identity/metadata) belongs to the registry entry, not to the
// freshly-loaded `plugin` -- preserve it across the move.
PluginDescriptor entry_descriptor = std::move(entry->descriptor);
*entry = std::move(plugin);
entry->descriptor = std::move(entry_descriptor);
committed = true;
}
}
}
}
if (!committed) {
// Outside the lock: this runs on_unload() and DECREFs the module.
plugin_loader::unload(plugin);
if (!cancelled)
fail(std::move(registry_error));
return;
}
clear_plugin_error(plugin_key);
{
std::lock_guard<std::mutex> lock(m_mutex);
m_load_errors.erase(plugin_key);
}
BOOST_LOG_TRIVIAL(info) << "[load_plugin_impl] SUCCESS plugin=" << plugin_key;
}
bool PluginManager::unload_plugin(const std::string& plugin_key)
{
auto notify_unload_complete = [this, &plugin_key]() {
if (!m_shutting_down.load(std::memory_order_acquire))
run_on_unload_callbacks(plugin_key);
};
Plugin removed;
bool cancelled = false;
bool found = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
// Cancel any in-progress load for this plugin so its worker discards the result.
cancelled = cancel_plugin_load_locked(plugin_key);
Plugin* plugin = find_plugin_locked(plugin_key);
if (plugin != nullptr && plugin->is_loaded()) {
found = true;
// Take the live parts out of the entry via Plugin's own move assignment; the
// descriptor stays behind, so the package remains discovered. Everything
// capability-shaped is discarded with the capabilities — the user's enable choices
// live on in the .install_state.json sidecar.
PluginDescriptor kept_descriptor = std::move(plugin->descriptor);
removed = std::move(*plugin);
plugin->descriptor = std::move(kept_descriptor);
}
}
if (!found) {
notify_plugin_load_state_changed(cancelled);
return true;
}
notify_unload_complete();
cancel_and_wait_for_capabilities(removed.capabilities);
notify_plugin_load_state_changed(cancelled);
// Teardown runs outside m_mutex: it calls into Python and must not hold the registry lock.
plugin_loader::unload(removed);
// 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;
return true;
}
void PluginManager::unload_all_plugins()
{
std::vector<Plugin> removed;
std::vector<std::string> removed_keys;
std::vector<std::shared_ptr<PluginCapabilityInterface>> capabilities;
{
std::lock_guard<std::mutex> lock(m_mutex);
for (Plugin& plugin : m_plugins) {
if (!plugin.is_loaded())
continue;
// Take the live parts out via Plugin's own move; the descriptor stays behind, as in
// unload_plugin() above.
PluginDescriptor kept_descriptor = std::move(plugin.descriptor);
Plugin taken = std::move(plugin);
plugin.descriptor = std::move(kept_descriptor);
removed.push_back(std::move(taken));
removed_keys.push_back(plugin.descriptor.plugin_key);
capabilities.insert(capabilities.end(), removed.back().capabilities.begin(), removed.back().capabilities.end());
}
}
// Bulk shutdown intentionally skips the normal unload notification path, but resources owned
// by GUI subscribers still need to be torn down before the interpreter is finalized.
for (const std::string& key : removed_keys)
run_on_unload_callbacks(key);
// Broadcast cancellation to every plugin before waiting. This keeps bulk unload from
// starving a later plugin while an earlier plugin uses the full cancellation grace period.
cancel_and_wait_for_capabilities(capabilities);
for (Plugin& plugin : removed)
plugin_loader::unload(plugin);
}
void PluginManager::unload_cloud_plugins()
{
std::vector<std::string> keys;
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const Plugin& plugin : m_plugins) {
if (plugin.is_loaded() && plugin.descriptor.is_cloud_plugin())
keys.push_back(plugin.descriptor.plugin_key);
}
}
// Release m_mutex before unloading: unload_plugin() re-acquires it and runs plugin teardown
// plus unload callbacks, which can re-enter the manager.
for (const std::string& key : keys)
unload_plugin(key);
}
// ── Enable state ────────────────────────────────────────────────────────────────────────────
void PluginManager::set_capability_enabled(const PluginCapabilityId& id, bool enabled)
{
PluginCapabilityId changed;
bool did_change = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
Plugin* plugin = find_plugin_locked(id.plugin_key);
if (plugin == nullptr || !plugin->is_loaded())
return;
for (const auto& capability : plugin->capabilities) {
if (!capability || capability->name() != id.name ||
(id.type != PluginCapabilityType::Unknown && capability->type() != id.type) ||
capability->is_enabled() == enabled)
continue;
capability->set_enabled(enabled);
changed = capability->identity();
did_change = true;
break;
}
}
if (!did_change)
return;
write_loaded_plugin_install_state(id.plugin_key);
if (enabled)
run_on_capability_load_callbacks(changed);
else
run_on_capability_unload_callbacks(changed);
}
void PluginManager::write_loaded_plugin_install_state(const std::string& plugin_key)
{
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
PluginDescriptor descriptor;
std::vector<std::pair<std::string, bool>> capabilities;
{
std::lock_guard<std::mutex> lock(m_mutex);
const Plugin* plugin = find_plugin_locked(plugin_key);
if (plugin == nullptr || !plugin->is_loaded())
return;
descriptor = plugin->descriptor;
for (const auto& capability : plugin->capabilities)
if (capability)
capabilities.emplace_back(capability->name(), capability->is_enabled());
}
if (descriptor.plugin_root.empty())
return;
write_install_state(boost::filesystem::path(descriptor.plugin_root), descriptor, /*enabled=*/true, capabilities);
}
void PluginManager::mark_plugin_install_state_disabled(const std::string& plugin_key)
{
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return;
const boost::filesystem::path root(descriptor.plugin_root);
PluginInstallState state;
if (!read_install_state(root, state)) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
}
state.enabled = false;
if (!write_install_state(root, state))
return;
std::lock_guard<std::mutex> lock(m_mutex);
if (Plugin* plugin = find_plugin_locked(plugin_key))
plugin->descriptor.enabled = false;
}
void PluginManager::revoke_plugin_permissions(const std::string& plugin_key)
{
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return;
const boost::filesystem::path root(descriptor.plugin_root);
PluginInstallState state;
if (!read_install_state(root, state)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to read install state for " << plugin_key;
return;
}
state.permissions = {};
if (!write_install_state(root, state))
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to revoke permissions for " << plugin_key;
}
// ── Callbacks ───────────────────────────────────────────────────────────────────────────────
void PluginManager::subscribe_on_load_callback(PluginLifecycleCompleteFn fn)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_callbacks[CallbackType::Load].push_back(std::move(fn));
}
void PluginManager::subscribe_on_unload_callback(PluginLifecycleCompleteFn fn)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_callbacks[CallbackType::Unload].push_back(std::move(fn));
}
void PluginManager::subscribe_on_capability_load_callback(CapabilityLifecycleFn fn)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_capability_callbacks[CallbackType::Load].push_back(std::move(fn));
}
void PluginManager::subscribe_on_capability_unload_callback(CapabilityLifecycleFn fn)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_capability_callbacks[CallbackType::Unload].push_back(std::move(fn));
}
void PluginManager::clear_callbacks()
{
std::lock_guard<std::mutex> lock(m_mutex);
m_callbacks.clear();
m_capability_callbacks.clear();
}
std::vector<PluginManager::PluginLifecycleCompleteFn> PluginManager::copy_callbacks(CallbackType type) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto it = m_callbacks.find(type);
return it == m_callbacks.end() ? std::vector<PluginLifecycleCompleteFn>{} : it->second;
}
std::vector<PluginManager::CapabilityLifecycleFn> PluginManager::copy_capability_callbacks(CallbackType type) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto it = m_capability_callbacks.find(type);
return it == m_capability_callbacks.end() ? std::vector<CapabilityLifecycleFn>{} : it->second;
}
// The run_on_*_callbacks below are called from detached load/unload workers. They snapshot the
// subscriber list under m_mutex (the copy protects the in-flight loop from a callback that
// subscribes mid-dispatch, and serialises against a concurrent subscribe), then hand the snapshot
// to the UI thread: subscribers touch wx and must not run on a worker, and none may run while the
// registry lock is held. The dispatch runs inline when already on the UI thread, else via
// CallAfter, since calling wx off the main thread is UB.
void PluginManager::run_on_load_callbacks(const std::string& plugin_key)
{
auto work = [callbacks = copy_callbacks(CallbackType::Load), plugin_key] {
for (auto& fn : callbacks) {
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;
}
}
};
if (wxTheApp == nullptr || wxIsMainThread())
work();
else
GUI::wxGetApp().CallAfter(std::move(work));
}
void PluginManager::run_on_unload_callbacks(const std::string& plugin_key)
{
auto work = [callbacks = copy_callbacks(CallbackType::Unload), plugin_key] {
for (auto& fn : callbacks) {
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";
}
}
};
if (wxTheApp == nullptr || wxIsMainThread())
work();
else
GUI::wxGetApp().CallAfter(std::move(work));
}
void PluginManager::run_on_capability_load_callbacks(const PluginCapabilityId& id)
{
auto work = [callbacks = copy_capability_callbacks(CallbackType::Load), id] {
for (auto& fn : callbacks) {
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;
}
}
};
if (wxTheApp == nullptr || wxIsMainThread())
work();
else
GUI::wxGetApp().CallAfter(std::move(work));
}
void PluginManager::run_on_capability_unload_callbacks(const PluginCapabilityId& id)
{
auto work = [callbacks = copy_capability_callbacks(CallbackType::Unload), id] {
for (auto& fn : callbacks) {
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;
}
}
};
if (wxTheApp == nullptr || wxIsMainThread())
work();
else
GUI::wxGetApp().CallAfter(std::move(work));
}
// ── Cloud user ──────────────────────────────────────────────────────────────────────────────
void PluginManager::set_cloud_user(const std::string& user_id)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_cloud_user_id = user_id;
}
// ── Errors ──────────────────────────────────────────────────────────────────────────────────
bool PluginManager::set_plugin_error(const std::string& plugin_key, std::string error)
{
std::string wait_error;
if (!wait_for_discovery(std::chrono::milliseconds::max(), wait_error))
return false;
std::lock_guard<std::mutex> lock(m_mutex);
Plugin* plugin = find_plugin_locked(plugin_key);
if (plugin == nullptr)
return false;
plugin->descriptor.set_error(std::move(error));
return true;
}
bool PluginManager::clear_plugin_error(const std::string& plugin_key)
{
std::string wait_error;
if (!wait_for_discovery(std::chrono::milliseconds::max(), wait_error))
return false;
std::lock_guard<std::mutex> lock(m_mutex);
Plugin* plugin = find_plugin_locked(plugin_key);
if (plugin == nullptr || !plugin->descriptor.is_metadata_valid())
return false;
plugin->descriptor.clear_error();
return true;
}
// ── Install / inspect ───────────────────────────────────────────────────────────────────────
bool PluginManager::inspect_local_plugin_package(const boost::filesystem::path& filepath,
PluginDescriptor& plugin_descriptor,
bool& existing_installation,
std::string& error) const
{ return plugin_loader::inspect_local_plugin_package(filepath, plugin_descriptor, existing_installation, error); }
bool PluginManager::install_plugin(const boost::filesystem::path& filepath, std::string& error)
{
PluginDescriptor descriptor{};
return install_plugin(filepath, descriptor, error);
}
bool PluginManager::install_plugin(const boost::filesystem::path& filepath, PluginDescriptor& plugin_descriptor, std::string& error)
{
error.clear();
std::string wait_error;
if (!wait_for_discovery(std::chrono::milliseconds::max(), wait_error)) {
error = wait_error.empty() ? "Plugin discovery is still running" : wait_error;
if (!plugin_descriptor.plugin_key.empty())
set_plugin_error(plugin_descriptor.plugin_key, error);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin installation failed while waiting for discovery: " << error;
return false;
}
std::string cloud_user_id;
{
std::lock_guard<std::mutex> lock(m_mutex);
cloud_user_id = m_cloud_user_id;
}
// A local overwrite must not leave the old module holding the package file or its Python
// namespace while the installer replaces the package on disk.
if (!plugin_descriptor.is_cloud_plugin()) {
const std::string local_plugin_key = make_local_plugin_key(filepath.stem().string());
if (is_plugin_loaded(local_plugin_key))
unload_plugin(local_plugin_key);
}
if (!plugin_loader::install_plugin(filepath, cloud_user_id, plugin_descriptor, error)) {
if (!plugin_descriptor.plugin_key.empty())
set_plugin_error(plugin_descriptor.plugin_key, error);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": " << error;
return false;
}
// Every successful install may have replaced executable plugin code. Revoke any permissions
// associated with the previous package so the newly installed version must request them again.
if (!plugin_descriptor.plugin_key.empty())
revoke_plugin_permissions(plugin_descriptor.plugin_key);
if (!plugin_descriptor.plugin_key.empty())
clear_plugin_error(plugin_descriptor.plugin_key);
return true;
}
// ── Cloud overlay ───────────────────────────────────────────────────────────────────────────
namespace {
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;
}
const char* const CLOUD_PLUGIN_NOT_FOUND_ERROR = "Plugin was not found in the cloud.";
} // namespace
void PluginManager::update_cloud_metadata(const std::vector<PluginDescriptor>& cloud_list)
{
std::lock_guard<std::mutex> lock(m_mutex);
// Collected and appended after the scan: push_back into m_plugins would invalidate the
// pointers the loop is holding.
std::vector<PluginDescriptor> new_entries;
for (const PluginDescriptor& cloud_entry : cloud_list) {
const 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;
}
const 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;
};
const auto existing = std::find_if(m_plugins.begin(), m_plugins.end(), [&matches_cloud_descriptor](const Plugin& plugin) {
return matches_cloud_descriptor(plugin.descriptor);
});
if (existing == m_plugins.end()) {
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;
new_entries.push_back(std::move(normalized_entry));
continue;
}
PluginDescriptor& entry = existing->descriptor;
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_ok = entry.is_metadata_valid();
const bool has_local_package = entry.has_local_package();
const std::string previous_error = entry.error;
// The cloud does not know about the local auto-load flag, which comes from the sidecar.
const bool enabled = entry.enabled;
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_ok : cloud_entry.metadata_valid;
entry.error = previous_error;
entry.enabled = enabled;
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_ok && !installed_version.empty() && !latest_version.empty() &&
is_cloud_version_newer(latest_version, installed_version);
if (entry.normalized_error() == CLOUD_PLUGIN_NOT_FOUND_ERROR)
entry.clear_error();
}
for (PluginDescriptor& descriptor : new_entries) {
Plugin plugin;
plugin.descriptor = std::move(descriptor);
m_plugins.push_back(std::move(plugin));
}
}
void PluginManager::clear_cloud_plugin_metadata()
{
// Cloud entries may own live Python modules. Unload them before erasing their vector entries,
// and do so outside m_mutex because both Python teardown and lifecycle callbacks can re-enter
// the manager.
unload_and_erase_if(
[](const Plugin& plugin) { return plugin.descriptor.is_cloud_plugin(); },
[this]() {
// A local plugin can still be carrying a stale "not found in the cloud" error from
// when it was cloud-tracked.
for (Plugin& plugin : m_plugins) {
if (plugin.descriptor.normalized_error() == CLOUD_PLUGIN_NOT_FOUND_ERROR)
plugin.descriptor.clear_error();
}
});
BOOST_LOG_TRIVIAL(info) << "Cleared cloud plugin metadata";
}
void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_found, std::vector<std::string>* out_unauthorized)
{
if (!m_cloud_service.can_fetch_cloud_plugins())
return;
std::vector<PluginDescriptor> cloud_list{};
std::vector<std::string> not_found{}, unauthorized{};
const bool cloud_fetch_succeeded = m_cloud_service.fetch_manifests_into_descriptors(cloud_list, not_found, unauthorized);
if (!cloud_fetch_succeeded) {
if (wxTheApp != nullptr) {
GUI::wxGetApp().CallAfter([] {
if (GUI::wxGetApp().is_closing())
return;
GUI::Plater* plater = GUI::wxGetApp().plater();
if (plater == nullptr || GUI::wxGetApp().imgui() == nullptr || !GUI::wxGetApp().imgui()->display_initialized())
return;
plater->get_notification_manager()->push_notification(GUI::NotificationType::CustomNotification,
GUI::NotificationManager::NotificationLevel::WarningNotificationLevel,
_u8L("Failed to fetch plugins from the cloud. See logs for details."));
});
}
}
if (cloud_fetch_succeeded)
update_cloud_metadata(cloud_list);
if (cloud_fetch_succeeded) {
std::lock_guard<std::mutex> lock(m_mutex);
// Clear the previous cloud verdicts before re-applying the fresh ones.
for (Plugin& plugin : m_plugins) {
PluginDescriptor& entry = plugin.descriptor;
if (!entry.is_cloud_plugin())
continue;
entry.set_unauthorized(false);
if (entry.cloud.has_value())
entry.cloud->orphaned = false;
if (entry.normalized_error() == CLOUD_PLUGIN_NOT_FOUND_ERROR)
entry.clear_error();
}
// A successful subscriptions response may report missing UUIDs explicitly, or it may
// simply omit an unsubscribed plugin from `data`. Both cases leave a locally retained
// cloud package orphaned. Owned plugins are returned by the separate mine endpoint and
// must not be orphaned merely because they are not subscribed.
for (Plugin& plugin : m_plugins) {
PluginDescriptor& entry = plugin.descriptor;
if (!entry.is_cloud_plugin() || entry.cloud->is_mine)
continue;
const bool explicitly_not_found = std::find(not_found.begin(), not_found.end(), entry.cloud_uuid()) != not_found.end();
const bool returned_by_cloud = std::any_of(cloud_list.begin(), cloud_list.end(), [&entry](const PluginDescriptor& cloud_entry) {
return cloud_entry.cloud_uuid() == entry.cloud_uuid();
});
entry.cloud->orphaned = explicitly_not_found || !returned_by_cloud;
if (entry.cloud->orphaned)
entry.cloud->update_available = false;
}
for (const std::string& uuid : unauthorized) {
for (Plugin& plugin : m_plugins) {
PluginDescriptor& entry = plugin.descriptor;
if (!entry.is_cloud_plugin() || entry.cloud_uuid() != uuid)
continue;
entry.set_unauthorized(true);
if (entry.cloud.has_value())
entry.cloud->update_available = false;
break;
}
}
}
if (out_not_found)
*out_not_found = std::move(not_found);
if (out_unauthorized)
*out_unauthorized = std::move(unauthorized);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Cloud plugin fetch: " << cloud_list.size() << " plugins returned";
}
bool PluginManager::subscribe_and_install_cloud_plugin(const std::string& plugin_key, std::string& error)
{
error.clear();
const std::string plugin_uuid = is_uuid(plugin_key) ? plugin_key : std::string{};
if (plugin_uuid.empty()) {
error = _u8L("Cloud plugin key is missing UUID.");
return false;
}
if (!m_cloud_service.can_fetch_cloud_plugins()) {
error = _u8L("Sign in to OrcaCloud to install this plugin.");
return false;
}
PluginDescriptor descriptor;
bool found = try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_cloud_plugin();
if (!found) {
fetch_plugins_from_cloud();
found = try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_cloud_plugin();
}
if (!found) {
if (!m_cloud_service.request_cloud_subscribe(plugin_uuid, error))
return false;
fetch_plugins_from_cloud();
found = try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_cloud_plugin();
if (!found) {
error = _u8L("Subscribed cloud plugin was not returned by OrcaCloud.");
return false;
}
}
if (descriptor.has_local_package())
return true;
return download_and_install_cloud_plugin(descriptor.plugin_key, descriptor.version, error);
}
bool PluginManager::download_and_install_cloud_plugin(const std::string& plugin_key, const std::string& version, std::string& error)
{
error.clear();
CloudPluginDownload download;
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor)) {
error = GUI::format(_u8L("Plugin not found: %1%"), plugin_key);
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to find plugin key " << plugin_key;
return false;
}
clear_plugin_error(plugin_key);
const std::string requested_version = version.empty() ? descriptor.version : version;
if (!m_cloud_service.download_cloud_plugin(descriptor, requested_version, download, error)) {
if (error.empty())
error = _u8L("Failed to download cloud plugin.");
set_plugin_error(plugin_key, error);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": " << error;
return false;
}
// Ensure cloud plugins install to _subscribed/<user_id>/
if (auto agent = m_cloud_service.get_cloud_agent()) {
const std::string user_id = agent->get_user_id();
if (!user_id.empty())
set_cloud_user(user_id);
}
// Record the version we just fetched from the cloud so install_plugin persists it to the
// install-state sidecar (the source of truth for the installed cloud version) instead of the
// local manifest/PEP723 header version.
descriptor.installed_version = requested_version;
if (!install_plugin(download.package_path, descriptor, error)) {
if (error.empty())
error = _u8L("Failed to install cloud plugin.");
set_plugin_error(plugin_key, error);
boost::system::error_code ec;
boost::filesystem::remove(download.package_path, ec);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": " << error;
return false;
}
PluginDescriptor updated_descriptor = descriptor;
if (!requested_version.empty())
updated_descriptor.version = requested_version;
// The version just downloaded and installed is now the locally installed version.
updated_descriptor.installed_version = requested_version.empty() ? descriptor.version : requested_version;
updated_descriptor.enabled = true;
if (updated_descriptor.cloud.has_value()) {
updated_descriptor.cloud->installed = true;
updated_descriptor.cloud->update_available = false;
updated_descriptor.cloud->unauthorized = false;
}
updated_descriptor.clear_error();
updated_descriptor.set_unauthorized(false);
{
std::lock_guard<std::mutex> lock(m_mutex);
Plugin* entry = find_plugin_locked(plugin_key);
if (entry == nullptr) {
error = _u8L("Plugin not found.");
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Cloud plugin " << plugin_key
<< " downloaded successfully but failed to update plugin metadata. Plugin object not found.";
} else {
entry->descriptor = std::move(updated_descriptor);
}
}
if (!error.empty()) {
set_plugin_error(plugin_key, error);
return false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Cloud plugin " << plugin_key << " installed successfully";
return true;
}
bool PluginManager::update_cloud_plugin(const std::string& plugin_key, std::string& error, std::string version)
{
error.clear();
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor)) {
error = GUI::format(_u8L("Plugin not found: %1%"), plugin_key);
return false;
}
if (!descriptor.is_cloud_plugin()) {
error = _u8L("Only cloud plugins can be updated.");
set_plugin_error(plugin_key, error);
return false;
}
// An empty version means "update the current plugin". If there is no update available, the
// intention is unclear.
if (version.empty()) {
if (descriptor.cloud->update_available) {
version = descriptor.latest_available_version();
} else {
error = _u8L("No update is available for this plugin.");
set_plugin_error(plugin_key, error);
return false;
}
}
clear_plugin_error(plugin_key);
if (descriptor.has_local_package()) {
boost::filesystem::path resolved_root;
bool resolved_allowed_plugin_root = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
resolved_allowed_plugin_root = resolve_allowed_plugin_root(descriptor, get_plugin_directories(m_cloud_user_id),
_u8L("Refusing to delete a plugin outside the known plugin directories."),
resolved_root, error);
}
if (!resolved_allowed_plugin_root) {
set_plugin_error(plugin_key, error);
return false;
}
unload_plugin(plugin_key);
if (!delete_plugin_root(resolved_root, plugin_key, error)) {
set_plugin_error(plugin_key, error);
return false;
}
}
if (!download_and_install_cloud_plugin(plugin_key, version, error)) {
set_plugin_error(plugin_key, error);
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": " << error;
return false;
}
clear_plugin_error(plugin_key);
return true;
}
// ── Delete / unsubscribe ────────────────────────────────────────────────────────────────────
bool PluginManager::delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error)
{
boost::filesystem::path resolved_root;
bool resolved_allowed_plugin_root = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
resolved_allowed_plugin_root = resolve_allowed_plugin_root(plugin, get_plugin_directories(m_cloud_user_id),
_u8L("Refusing to delete a plugin outside the known plugin directories."),
resolved_root, error);
}
if (!resolved_allowed_plugin_root)
return false;
unload_plugin(plugin.plugin_key);
if (!delete_plugin_root(resolved_root, plugin.plugin_key, error))
return false;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_plugins.erase(std::remove_if(m_plugins.begin(), m_plugins.end(),
[&plugin](const Plugin& entry) { return entry.descriptor.plugin_key == plugin.plugin_key; }),
m_plugins.end());
}
return true;
}
bool PluginManager::keep_installed_plugin_as_local(const PluginDescriptor& plugin_descriptor, std::string& error)
{
namespace fs = boost::filesystem;
boost::filesystem::path resolved_root;
bool resolved_allowed_plugin_root = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
resolved_allowed_plugin_root = resolve_allowed_plugin_root(plugin_descriptor, get_plugin_directories(m_cloud_user_id),
_u8L("Refusing to update a plugin outside the known plugin directories."),
resolved_root, error);
}
if (!resolved_allowed_plugin_root)
return false;
const std::string old_key = plugin_descriptor.plugin_key;
// Generate a new local key from the entry file stem (cloud -> local conversion).
const std::string entry_stem = fs::path(plugin_descriptor.entry_path).stem().string();
const std::string new_key = make_local_plugin_key(!entry_stem.empty() ? entry_stem : resolved_root.stem().string());
PluginDescriptor local_descriptor = plugin_descriptor;
local_descriptor.plugin_key = new_key;
local_descriptor.cloud = std::nullopt;
local_descriptor.clear_error();
// Re-key the entry in place, carrying the live module/capabilities (if any) with it. The
// capabilities' audit key must follow, since it is what identifies their owning package.
std::vector<PluginCapabilityId> rekeyed;
{
std::lock_guard<std::mutex> lock(m_mutex);
Plugin* entry = find_plugin_locked(old_key);
if (entry == nullptr) {
error = GUI::format(_u8L("Plugin manifest not found: %1%"), old_key);
return false;
}
if (find_plugin_locked(new_key) != nullptr && new_key != old_key) {
error = GUI::format(_u8L("Cannot keep plugin local: local plugin key already exists: %1%"), new_key);
BOOST_LOG_TRIVIAL(warning) << error;
return false;
}
// Preserve the sidecar's package and capability choices while changing only its origin.
// Re-keying an installed plugin must not silently re-enable capabilities.
PluginInstallState install_state;
if (!read_install_state(resolved_root, install_state)) {
install_state.installed_version = !local_descriptor.installed_version.empty()
? local_descriptor.installed_version
: local_descriptor.version;
install_state.enabled = local_descriptor.enabled;
}
install_state.installed_from = "local";
install_state.plugin_name = local_descriptor.name;
install_state.cloud_uuid.clear();
if (install_state.installed_version.empty())
install_state.installed_version = local_descriptor.version;
if (!write_install_state(resolved_root, install_state)) {
error = GUI::format(_u8L("Failed to update plugin install state: %1%"), (resolved_root / INSTALL_STATE_FILE).string());
return false;
}
entry->descriptor.plugin_key = new_key;
entry->descriptor.cloud.reset();
entry->descriptor.clear_error();
for (const auto& capability : entry->capabilities) {
if (capability)
capability->set_audit_plugin_key(new_key);
}
for (const auto& capability : entry->capabilities) {
if (capability && capability->is_enabled())
rekeyed.push_back(PluginCapabilityId{capability->type(), capability->name(), new_key});
}
}
// Subscribers 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 manager.
for (const PluginCapabilityId& id : rekeyed) {
run_on_capability_unload_callbacks(PluginCapabilityId{id.type, id.name, old_key});
run_on_capability_load_callbacks(id);
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Transitioned plugin from " << old_key << " to " << new_key;
return true;
}
bool PluginManager::finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error)
{
if (keep_local && plugin.has_local_package()) {
if (!keep_installed_plugin_as_local(plugin, error))
return false;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Removed cloud tracking, kept local copy: " << plugin.plugin_key;
return true;
}
if (plugin.has_local_package()) {
if (!delete_installed_plugin_package(plugin, error))
return false;
fetch_plugins_from_cloud();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Deleted local package after cloud removal: " << plugin.plugin_key;
return true;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_plugins.erase(std::remove_if(m_plugins.begin(), m_plugins.end(),
[&plugin](const Plugin& entry) { return entry.descriptor.plugin_key == plugin.plugin_key; }),
m_plugins.end());
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Removed cloud-only plugin: " << plugin.plugin_key;
return true;
}
bool PluginManager::delete_plugin(const std::string& plugin_key, std::string& error)
{
if (!wait_for_discovery(std::chrono::milliseconds::max(), error))
return false;
error.clear();
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor)) {
error = GUI::format(_u8L("Plugin not found: %1%"), plugin_key);
return false;
}
if (!delete_installed_plugin_package(descriptor, error)) {
set_plugin_error(plugin_key, error);
return false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Deleted plugin: " << plugin_key;
return true;
}
bool PluginManager::unsubscribe_cloud_plugin(const std::string& plugin_key, std::string& error)
{
if (!wait_for_discovery(std::chrono::milliseconds::max(), error))
return false;
error.clear();
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor)) {
error = GUI::format(_u8L("Plugin not found: %1%"), plugin_key);
return false;
}
if (!descriptor.is_cloud_plugin()) {
error = _u8L("Only cloud plugins can be unsubscribed.");
set_plugin_error(plugin_key, error);
return false;
}
if (descriptor.cloud && descriptor.cloud->is_mine) {
error = _u8L("Cannot unsubscribe your own plugins.");
set_plugin_error(plugin_key, error);
return false;
}
if (!m_cloud_service.request_cloud_unsubscribe(descriptor, error)) {
set_plugin_error(plugin_key, error);
return false;
}
return finalize_cloud_plugin_removal(descriptor, true, error);
}
bool PluginManager::delete_and_unsubscribe_cloud_plugin(const std::string& plugin_key, std::string& error)
{
if (!wait_for_discovery(std::chrono::milliseconds::max(), error))
return false;
error.clear();
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor)) {
error = GUI::format(_u8L("Plugin not found: %1%"), plugin_key);
return false;
}
if (!descriptor.is_cloud_plugin()) {
error = _u8L("Only cloud plugins can be deleted and unsubscribed.");
set_plugin_error(plugin_key, error);
return false;
}
if (descriptor.cloud->is_mine) {
error = _u8L("Cannot unsubscribe your own plugins. Use Delete to remove the local files.");
set_plugin_error(plugin_key, error);
return false;
}
unload_plugin(plugin_key);
if (!m_cloud_service.request_cloud_unsubscribe(descriptor, error)) {
set_plugin_error(plugin_key, error);
return false;
}
return finalize_cloud_plugin_removal(descriptor, false, error);
}
ExecutionResult PluginManager::run_script_capability(const std::string& plugin_key, const std::string& capability_name, std::string& error)
{
if (plugin_key.empty() || capability_name.empty()) {
return {};
}
auto cap = get_plugin_capability({PluginCapabilityType::Script, capability_name, plugin_key});
if (!cap)
return {};
auto cap_interface = std::dynamic_pointer_cast<ScriptPluginCapability>(cap);
if (!cap_interface)
return {};
ExecutionResult result;
try {
PythonGILState gil;
if (!gil) {
error = "Python interpreter is shutting down";
return {};
}
result = cap_interface->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 = _u8L("Unknown error");
BOOST_LOG_TRIVIAL(error) << "Script plugin execution threw unknown exception. plugin_key=" << plugin_key;
}
if (!error.empty()) {
set_plugin_error(plugin_key, error);
}
return result;
}
void PluginManager::dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx) {
const auto is_canceled = [&ctx]() {
return ctx.cancellation_check && ctx.cancellation_check();
};
if (is_canceled())
return;
for (const auto& cap : get_plugin_capabilities()) {
if (!cap || !cap->is_enabled()) continue;
if (is_canceled())
break;
try {
cap->on_lifecycle_event(evt, ctx);
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name()
<< "' on_lifecycle_event(" << lifecycle_event_to_string(evt) << ") threw: " << ex.what()
<< " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code)
<< ", msg='" << ctx.msg << "']";
} catch (...) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name()
<< "' on_lifecycle_event(" << lifecycle_event_to_string(evt)
<< ") threw a non-standard exception"
<< " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code)
<< ", msg='" << ctx.msg << "']";
}
}
}
} // namespace Slic3r