mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-23 17:20:36 +03:00
Compare commits
12 Commits
fix/cloud-
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7caf0db07 | ||
|
|
f1f65342f0 | ||
|
|
738af9f4eb | ||
|
|
bbe39629ed | ||
|
|
bc6ae243ca | ||
|
|
b121051220 | ||
|
|
4c2d9b4b18 | ||
|
|
28feb8480e | ||
|
|
242e100feb | ||
|
|
2a46bd1384 | ||
|
|
ddbbc7f7f2 | ||
|
|
3a48086f5d |
@@ -48,13 +48,14 @@ std::string plugin_defaults_user_script()
|
||||
return WebViewHostDialog::document_start_injector(css, "orca-plugin-defaults", "beforeend");
|
||||
}
|
||||
|
||||
// Injected into every page at document start (before the plugin's own scripts).
|
||||
// Defines window.orca as the only host surface the page may use. It references
|
||||
// window.wx lazily (at call time) so it never races the backend's deferred
|
||||
// registration of the "wx" message handler. Guarded against double-injection so
|
||||
// it is harmless if also prepended.
|
||||
// Injected into the top-level page at document start (before the plugin's own
|
||||
// scripts). Defines window.orca as the only host surface the page may use. It
|
||||
// references window.wx lazily (at call time) so it never races the backend's
|
||||
// deferred registration of the "wx" message handler. Guarded against
|
||||
// double-injection so it is harmless if also prepended.
|
||||
constexpr char ORCA_BRIDGE_JS[] = R"JS(
|
||||
(function () {
|
||||
if (window.top !== window.self) return;
|
||||
if (window.orca) return;
|
||||
var handlers = [];
|
||||
function send(kind, data) {
|
||||
|
||||
@@ -300,7 +300,12 @@ PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
|
||||
|
||||
add_action("open_folder", "Show in folder", has_local);
|
||||
|
||||
add_action("reinstall_plugin", "Reinstall");
|
||||
if (is_cloud) {
|
||||
add_action("reinstall_plugin", "Reinstall");
|
||||
} else {
|
||||
add_action("reload_plugin", "Reload");
|
||||
add_action("clear_cache_reload_plugin", "Delete cache and reload");
|
||||
}
|
||||
|
||||
return available_actions;
|
||||
}
|
||||
@@ -739,11 +744,13 @@ void PluginsDialog::handle_plugin_menu_action(const std::string& plugin_key, con
|
||||
unsubscribe_cloud_plugin(row_data);
|
||||
} else if (action == "delete_mine_plugin") {
|
||||
delete_mine_local_and_cloud_plugin(plugin_key);
|
||||
} else if (action == "reload_plugin") {
|
||||
reload_local_plugin(plugin_key, /*clear_cache=*/false);
|
||||
} else if (action == "clear_cache_reload_plugin") {
|
||||
reload_local_plugin(plugin_key, /*clear_cache=*/true);
|
||||
} else if (action == "reinstall_plugin") {
|
||||
if (row_data.is_cloud_plugin())
|
||||
reinstall_cloud_plugin(row_data);
|
||||
else
|
||||
reinstall_local_plugin(plugin_key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1123,7 +1130,7 @@ void PluginsDialog::unsubscribe_cloud_plugin(const PluginDescriptor& plugin)
|
||||
_L("Unsubscribing plugin"), _L("Deleting local files and unsubscribing plugin..."));
|
||||
}
|
||||
|
||||
void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
|
||||
void PluginsDialog::reload_local_plugin(const std::string& plugin_key, bool clear_cache)
|
||||
{
|
||||
if (plugin_key.empty())
|
||||
return;
|
||||
@@ -1132,11 +1139,34 @@ void PluginsDialog::reinstall_local_plugin(const std::string& plugin_key)
|
||||
std::pair<bool, std::string> reload_result{false, ""};
|
||||
try {
|
||||
reload_result = run_with_dialog_wait(
|
||||
[plugin_key, was_loaded]() -> std::pair<bool, std::string> {
|
||||
[plugin_key, was_loaded, clear_cache]() -> std::pair<bool, std::string> {
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
|
||||
boost::filesystem::path cache_dir;
|
||||
if (clear_cache) {
|
||||
PluginDescriptor descriptor;
|
||||
if (!manager.try_get_plugin_descriptor(plugin_key, descriptor))
|
||||
return {false, "Plugin not found."};
|
||||
|
||||
boost::filesystem::path resolved_root;
|
||||
std::string resolve_error;
|
||||
if (!resolve_allowed_plugin_root(descriptor, {get_orca_plugins_dir()},
|
||||
"Refusing to clear a plugin cache outside the local plugin directory.",
|
||||
resolved_root, resolve_error))
|
||||
return {false, resolve_error};
|
||||
cache_dir = resolved_root / "__whl_extracted__";
|
||||
}
|
||||
|
||||
if (!manager.unload_plugin(plugin_key))
|
||||
return {false, "Failed to unload plugin."};
|
||||
|
||||
if (clear_cache) {
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove_all(cache_dir, ec);
|
||||
if (ec)
|
||||
return {false, "Failed to clear plugin cache: " + ec.message()};
|
||||
}
|
||||
|
||||
manager.load_plugin(plugin_key, false);
|
||||
std::string error;
|
||||
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) || !manager.is_plugin_loaded(plugin_key))
|
||||
|
||||
@@ -96,7 +96,7 @@ private:
|
||||
void open_plugin_folder(const Slic3r::PluginDescriptor& plugin);
|
||||
void delete_local_plugin(const Slic3r::PluginDescriptor& plugin);
|
||||
void unsubscribe_cloud_plugin(const Slic3r::PluginDescriptor& plugin);
|
||||
void reinstall_local_plugin(const std::string& plugin_key);
|
||||
void reload_local_plugin(const std::string& plugin_key, bool clear_cache);
|
||||
void reinstall_cloud_plugin(const Slic3r::PluginDescriptor& plugin);
|
||||
void delete_mine_local_and_cloud_plugin(const std::string& plugin_key);
|
||||
|
||||
|
||||
@@ -96,6 +96,9 @@ std::string WebViewHostDialog::document_start_injector(const std::string& markup
|
||||
const std::string literal = nlohmann::json(markup).dump();
|
||||
std::string s;
|
||||
s += "(function(){";
|
||||
// wxWebView's AddUserScript runs in child frames too (including cross-origin
|
||||
// frames on WebView2). Host theme state belongs only to the top-level page.
|
||||
s += "if(window.top!==window.self)return;";
|
||||
s += prelude;
|
||||
s += "var css=" + literal + ";";
|
||||
s += "function inject(){";
|
||||
|
||||
@@ -54,7 +54,7 @@ struct PluginDescriptor
|
||||
std::string description; // Plugin description
|
||||
std::string author; // Plugin author from manifest, if available
|
||||
std::string version; // Selected plugin version
|
||||
std::string latest_version; // Latest available cloud version fallback when changelog is unavailable.
|
||||
std::string latest_version; // Authoritative latest available cloud version.
|
||||
std::string installed_version; // Locally installed package version. Preserved across cloud merges, which overwrite `version` with the latest cloud version. Empty when not installed.
|
||||
std::vector<std::string> display_types; // Display-only "compatibility" labels (cloud: raw service labels; local: from real capabilities). Never used for dispatch.
|
||||
std::string plugin_root; // Installed plugin directory, even when entry_path is invalid or ambiguous.
|
||||
@@ -66,6 +66,10 @@ struct PluginDescriptor
|
||||
std::string error; // Blocking error message. Non-empty means the plugin is in an error state.
|
||||
std::optional<CloudPluginState> cloud; // Extra cloud state layered on top of a normal plugin descriptor.
|
||||
bool metadata_valid = false; // Manifest/package validity stays separate from the user-facing error field.
|
||||
// Whether the discovery pass successfully read .install_state.json. This is transient scan
|
||||
// metadata, used to distinguish an explicit enabled=false from a sidecar that was unavailable
|
||||
// during a live package replacement.
|
||||
bool install_state_valid = false;
|
||||
// Package auto-load flag, read from .install_state.json. Defaults to FALSE: a package with no
|
||||
// sidecar has never been installed through Orca and carries no auto-load intent, so it must not
|
||||
// be loaded at startup. Installing a package writes the sidecar with enabled = true.
|
||||
@@ -113,10 +117,6 @@ struct PluginDescriptor
|
||||
bool is_unauthorized() const { return get_update_status() == PluginUpdateStatus::Unauthorized; }
|
||||
std::string latest_available_version() const
|
||||
{
|
||||
for (const PluginChangelog& entry : changelog) {
|
||||
if (!entry.version.empty())
|
||||
return entry.version;
|
||||
}
|
||||
if (!latest_version.empty())
|
||||
return latest_version;
|
||||
return version;
|
||||
|
||||
@@ -172,7 +172,7 @@ void scan_plugin_directory(const std::string& dir_path, std::vector<PluginDescri
|
||||
// its error, rather than dropping it silently.
|
||||
if (entry_path.empty()) {
|
||||
descriptor.set_error(entry_error);
|
||||
read_install_state(plugin_dir, descriptor);
|
||||
descriptor.install_state_valid = read_install_state(plugin_dir, descriptor);
|
||||
assign_discovered_plugin_key(descriptor, plugin_dir);
|
||||
out.push_back(std::move(descriptor));
|
||||
BOOST_LOG_TRIVIAL(warning) << "Invalid plugin package: " << plugin_dir.string() << " - " << out.back().error;
|
||||
@@ -185,7 +185,7 @@ void scan_plugin_directory(const std::string& dir_path, std::vector<PluginDescri
|
||||
read_python_plugin_metadata(entry_path, descriptor, meta_error);
|
||||
if (!parsed) {
|
||||
descriptor.set_error(meta_error);
|
||||
read_install_state(plugin_dir, descriptor);
|
||||
descriptor.install_state_valid = read_install_state(plugin_dir, descriptor);
|
||||
assign_discovered_plugin_key(descriptor, entry_path);
|
||||
out.push_back(std::move(descriptor));
|
||||
BOOST_LOG_TRIVIAL(warning) << (is_wheel ? "Invalid wheel plugin: " : "Invalid .py plugin: ")
|
||||
@@ -199,7 +199,7 @@ void scan_plugin_directory(const std::string& dir_path, std::vector<PluginDescri
|
||||
|
||||
// Cloud identity and the package-level auto-load flag. plugin_key is always derived
|
||||
// below, never read from the sidecar.
|
||||
read_install_state(plugin_dir, descriptor);
|
||||
descriptor.install_state_valid = read_install_state(plugin_dir, descriptor);
|
||||
assign_discovered_plugin_key(descriptor, entry_path);
|
||||
|
||||
out.push_back(std::move(descriptor));
|
||||
@@ -735,11 +735,11 @@ bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boo
|
||||
return true;
|
||||
}
|
||||
|
||||
void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry)
|
||||
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry)
|
||||
{
|
||||
PluginInstallState state;
|
||||
if (!read_install_state(plugin_dir, state))
|
||||
return;
|
||||
return false;
|
||||
|
||||
// The cloud identity and the persisted installed version are read back. plugin_key
|
||||
// is always derived by the catalog scan (filename for local, the cloud uuid for
|
||||
@@ -756,6 +756,7 @@ void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescrip
|
||||
// capability has no existence — and so no state — until it is materialized, at which point the
|
||||
// loader seeds the flag onto the capability itself.
|
||||
entry.enabled = state.enabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out)
|
||||
|
||||
@@ -156,8 +156,9 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
|
||||
// Convenience overload: write(dir, entry, /*enabled=*/true, /*capabilities=*/{}).
|
||||
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry);
|
||||
|
||||
// Reads only the cloud identity (uuid) back into the descriptor; plugin_key is always derived.
|
||||
void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry);
|
||||
// Reads install state back into the descriptor; plugin_key is always derived. Returns whether the
|
||||
// sidecar was present and valid.
|
||||
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry);
|
||||
// Full read of the sidecar; returns false if there is no/invalid sidecar.
|
||||
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out);
|
||||
|
||||
|
||||
@@ -321,6 +321,8 @@ void PluginManager::merge_discovered_plugins(std::vector<PluginDescriptor> disco
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,59 @@
|
||||
#include "PluginHost.hpp"
|
||||
#include "PluginHostBindings.hpp"
|
||||
#include "PluginHostUi.hpp"
|
||||
#include <slic3r/plugin/PluginAuditManager.hpp>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
#include <slic3r/plugin/PluginFsUtils.hpp>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace host_bindings {
|
||||
void register_plugin(pybind11::module_& host)
|
||||
{
|
||||
auto plugin_host = host.def_submodule("plugin", "Plugin host API");
|
||||
|
||||
plugin_host.def(
|
||||
"storage",
|
||||
[]() -> std::string {
|
||||
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
|
||||
if (plugin_key.empty())
|
||||
throw std::runtime_error("plugin.storage() must be called from a plugin callback");
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor))
|
||||
throw std::runtime_error("The current plugin is not registered");
|
||||
|
||||
// plugin_root is populated for installed packages. If it is unavailable, the entry
|
||||
// path still identifies the same package directory. This is important for local
|
||||
// plugins: their directory is based on the source filename (including its extension),
|
||||
// while plugin_key is based on the filename stem.
|
||||
const boost::filesystem::path plugin_root = resolve_plugin_root_from_descriptor(descriptor);
|
||||
if (!plugin_root.empty())
|
||||
return plugin_root.string();
|
||||
|
||||
if (!descriptor.is_cloud_plugin())
|
||||
throw std::runtime_error("The current local plugin folder is unavailable");
|
||||
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().getAgent() == nullptr)
|
||||
throw std::runtime_error("Cloud plugin storage is unavailable before networking is initialized");
|
||||
|
||||
const std::string user_id = GUI::wxGetApp().getAgent()->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");
|
||||
|
||||
return (boost::filesystem::path(get_cloud_plugin_dir(user_id)) / plugin_key).string();
|
||||
},
|
||||
"Return the installed folder of the current plugin.");
|
||||
}
|
||||
} // namespace host_bindings
|
||||
|
||||
void PluginHost::RegisterBindings(pybind11::module_& module)
|
||||
{
|
||||
auto host = module.def_submodule("host", "Host application API");
|
||||
@@ -15,6 +65,7 @@ void PluginHost::RegisterBindings(pybind11::module_& module)
|
||||
host_bindings::register_presets(host);
|
||||
host_bindings::register_model(host);
|
||||
host_bindings::register_app(host);
|
||||
host_bindings::register_plugin(host);
|
||||
|
||||
// UI: native dialogs and interactive HTML windows for plugins.
|
||||
PluginHostUi::RegisterBindings(host);
|
||||
|
||||
@@ -12,5 +12,5 @@ void register_presets(pybind11::module_& host); // PluginHostPresets.cpp
|
||||
void register_model(pybind11::module_& host); // PluginHostModel.cpp
|
||||
void register_app(pybind11::module_& host); // PluginHostApp.cpp
|
||||
void register_slicing(pybind11::module_& host); // PluginHostSlicing.cpp
|
||||
|
||||
void register_plugin(pybind11::module_& host); // PluginHost.cpp
|
||||
} // namespace Slic3r::host_bindings
|
||||
|
||||
@@ -52,6 +52,29 @@ print('ok')
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("plugin latest version uses the authoritative catalog field", "[PluginDescriptor]")
|
||||
{
|
||||
PluginDescriptor descriptor;
|
||||
descriptor.version = "1.3.0";
|
||||
descriptor.latest_version = "1.3.0";
|
||||
PluginChangelog changelog;
|
||||
changelog.version = "1.2.0";
|
||||
descriptor.changelog.push_back(changelog);
|
||||
|
||||
CHECK(descriptor.latest_available_version() == "1.3.0");
|
||||
}
|
||||
|
||||
TEST_CASE("plugin latest version falls back to the descriptor version", "[PluginDescriptor]")
|
||||
{
|
||||
PluginDescriptor descriptor;
|
||||
descriptor.version = "1.1.0";
|
||||
PluginChangelog changelog;
|
||||
changelog.version = "1.0.0";
|
||||
descriptor.changelog.push_back(changelog);
|
||||
|
||||
CHECK(descriptor.latest_available_version() == "1.1.0");
|
||||
}
|
||||
|
||||
// Regression: update_cloud_metadata() replaces a matched entry's descriptor wholesale with the
|
||||
// cloud catalog record (`entry = cloud_entry`). Configuration used to ride on the descriptor, so
|
||||
// that overwrite silently wiped it and plugins fell back to their built-in defaults (found via
|
||||
|
||||
Reference in New Issue
Block a user