Merge branch 'main' into feat/plugin-auditing

This commit is contained in:
Ian Chua
2026-08-14 12:33:08 +08:00
parent ddd62e25df
commit d99f4c8164
750 changed files with 33478 additions and 6833 deletions
+3
View File
@@ -20,6 +20,7 @@ struct CloudPluginState
bool update_available = false; // Cloud version > the local package version.
bool unauthorized = false; // Cloud plugin is valid locally, but cannot receive cloud updates.
bool is_mine = false; // Plugin was created (and uploaded) by the current user.
bool orphaned = false; // Cloud identity remains locally, but the plugin is no longer subscribed/available.
};
enum class PluginUpdateStatus
@@ -106,6 +107,8 @@ struct PluginDescriptor
{
if (!cloud.has_value())
return PluginUpdateStatus::Normal;
if (cloud->orphaned)
return PluginUpdateStatus::Normal;
if (cloud->unauthorized)
return PluginUpdateStatus::Unauthorized;
if (cloud->update_available)
+1 -2
View File
@@ -120,8 +120,7 @@ bool delete_plugin_root(const boost::filesystem::path& resolved_root, const std:
}
if (removed_count == 0) {
error = "Plugin folder was not found: " + resolved_root.string();
return false;
return true;
}
BOOST_LOG_TRIVIAL(info) << "Deleted plugin: " << plugin_id << " from " << resolved_root.string();
+65 -18
View File
@@ -311,6 +311,7 @@ void PluginManager::merge_discovered_plugins(std::vector<PluginDescriptor> disco
}
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) {
@@ -331,12 +332,47 @@ void PluginManager::merge_discovered_plugins(std::vector<PluginDescriptor> disco
return;
}
// Unloading may call Python and lifecycle subscribers may re-enter the manager, so never do it
// while holding m_mutex. unload_and_erase_if() retries until no matching entry is loaded at the
// moment of erase, in case another caller starts a load between the initial snapshot and the
// teardown.
unload_and_erase_if(
[&seen](const Plugin& plugin) { return std::find(seen.begin(), seen.end(), plugin.descriptor.plugin_key) == seen.end(); });
// 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,
@@ -1464,7 +1500,8 @@ void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_f
std::vector<PluginDescriptor> cloud_list{};
std::vector<std::string> not_found{}, unauthorized{};
if (!m_cloud_service.fetch_manifests_into_descriptors(cloud_list, 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())
@@ -1479,9 +1516,10 @@ void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_f
}
}
update_cloud_metadata(cloud_list);
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.
@@ -1490,19 +1528,28 @@ void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_f
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();
}
for (const std::string& uuid : not_found) {
for (Plugin& plugin : m_plugins) {
PluginDescriptor& entry = plugin.descriptor;
if (!entry.is_cloud_plugin() || entry.cloud_uuid() != uuid)
continue;
if (!entry.has_local_package())
entry.set_error(CLOUD_PLUGIN_NOT_FOUND_ERROR);
break;
}
// 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) {
+6
View File
@@ -132,6 +132,11 @@ public:
bool try_get_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& out) const;
// Same, but only for packages that are loadable (i.e. not an invalid package).
bool try_get_valid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& out) const;
// Packages that were present in the previous discovery pass but were not found on disk in the
// latest rescan. They are retained until the user explicitly removes them or a later scan finds
// them again.
std::vector<PluginDescriptor> get_missing_plugin_descriptors() const;
void remove_missing_plugins(const std::vector<std::string>& plugin_keys);
// Packages whose .install_state.json marks them for auto-load.
std::vector<std::string> get_enabled_plugin_keys() const;
// The package owning a loaded capability, for the by-name dispatch path.
@@ -265,6 +270,7 @@ private:
// Every discovered plugin, loaded or not. module == nullptr => not loaded.
std::vector<Plugin> m_plugins;
std::unordered_set<std::string> m_missing_plugin_keys;
std::unordered_set<std::string> m_load_in_progress;
// Keys whose in-flight load has been cancelled. Cancellation does NOT remove the key from
+10
View File
@@ -2,6 +2,7 @@
#include <libslic3r/Model.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <slic3r/GUI/GUI.hpp>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/Plater.hpp>
@@ -55,6 +56,15 @@ void host_bindings::register_app(py::module_& host)
return current_plater()->model();
}, py::return_value_policy::reference);
host.def("preset_bundle", &current_preset_bundle, py::return_value_policy::reference);
// UI language of the running app ("en_US", "ru_RU", ...), so plugins can
// localize their own dialogs. The app config file that stores this value
// is deny-listed by the audit hook (it sits next to cloud secrets), so a
// read-only accessor is the supported way to get just the language.
host.def("app_language", []() -> std::string {
if (wxTheApp == nullptr)
throw std::runtime_error("OrcaSlicer application is not initialized");
return GUI::into_u8(GUI::wxGetApp().current_language_code_safe());
});
}
} // namespace Slic3r