mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-23 09:10:30 +03:00
Compare commits
11 Commits
fix/cloud-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1f65342f0 | ||
|
|
738af9f4eb | ||
|
|
bbe39629ed | ||
|
|
bc6ae243ca | ||
|
|
b121051220 | ||
|
|
4c2d9b4b18 | ||
|
|
28feb8480e | ||
|
|
242e100feb | ||
|
|
2a46bd1384 | ||
|
|
ddbbc7f7f2 | ||
|
|
3a48086f5d |
@@ -601,17 +601,15 @@ function SourceLabel(source) {
|
||||
return "Mine";
|
||||
case "subscribed":
|
||||
return "Subscribed";
|
||||
case "orphaned":
|
||||
return "Orphaned";
|
||||
default:
|
||||
return "Local";
|
||||
}
|
||||
}
|
||||
|
||||
// Shared source pill, used both after the row name and in the info panel.
|
||||
// Shared Local/Subscribed/Mine pill, used both after the row name and in the info panel.
|
||||
function SourceBadge(source) {
|
||||
const normalized = String(source || "").toLowerCase();
|
||||
const variant = (normalized === "mine" || normalized === "subscribed" || normalized === "orphaned") ? normalized : "local";
|
||||
const variant = (normalized === "mine" || normalized === "subscribed") ? normalized : "local";
|
||||
const badge = document.createElement("span");
|
||||
badge.className = `plugin-source-badge source-${variant}`;
|
||||
badge.textContent = SourceLabel(source);
|
||||
@@ -655,7 +653,7 @@ function LabelCell(plugin, isExpanded = false, capabilityCount = 0, nameRanges =
|
||||
const labelCell = document.createElement("span");
|
||||
labelCell.className = "label-cell";
|
||||
|
||||
const hasCloudLink = plugin.source === "mine" || plugin.source === "subscribed" || plugin.source === "orphaned";
|
||||
const hasCloudLink = plugin.source === "mine" || plugin.source === "subscribed";
|
||||
const pluginLabelText = plugin.label || plugin.name || plugin.plugin_id || "";
|
||||
const canExpand = capabilityCount > 0;
|
||||
|
||||
@@ -706,7 +704,7 @@ function LabelCell(plugin, isExpanded = false, capabilityCount = 0, nameRanges =
|
||||
function SourceCell(plugin) {
|
||||
const cell = document.createElement("span");
|
||||
const normalized = String(plugin.source || "").toLowerCase();
|
||||
const variant = (normalized === "mine" || normalized === "subscribed" || normalized === "orphaned") ? normalized : "local";
|
||||
const variant = (normalized === "mine" || normalized === "subscribed") ? normalized : "local";
|
||||
cell.className = `source-cell source-${variant}`;
|
||||
|
||||
const sourceLabel = document.createElement("span");
|
||||
@@ -1267,7 +1265,7 @@ function RenderDescription(plugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCloud = plugin && (plugin.source === "mine" || plugin.source === "subscribed" || plugin.source === "orphaned");
|
||||
const isCloud = plugin && (plugin.source === "mine" || plugin.source === "subscribed");
|
||||
if (isCloud && String(plugin?.sharing_token || "")) {
|
||||
node.appendChild(document.createTextNode("View on OrcaCloud "));
|
||||
const link = document.createElement("a");
|
||||
@@ -1404,13 +1402,6 @@ function RenderDetailSummary(container, plugin) {
|
||||
message.textContent = errorText || StatusDescription(plugin);
|
||||
container.appendChild(message);
|
||||
|
||||
if (plugin.orphaned === true) {
|
||||
const warning = document.createElement("div");
|
||||
warning.className = "detail-description detail-warning-text";
|
||||
warning.textContent = "Orphaned: This plugin is no longer subscribed or available in OrcaCloud. The local copy remains installed and can still be used.";
|
||||
container.appendChild(warning);
|
||||
}
|
||||
|
||||
const updateStatus = GetUpdateStatus(plugin);
|
||||
if (updateStatus === "update_available") {
|
||||
const note = document.createElement("div");
|
||||
|
||||
@@ -251,11 +251,6 @@ body.pane-resizing {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.source-cell.source-orphaned {
|
||||
color: var(--plugin-status-warn);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.source-cell.source-local {
|
||||
color: var(--plugin-source-neutral-text);
|
||||
}
|
||||
@@ -653,10 +648,6 @@ body.pane-resizing {
|
||||
color: var(--plugin-status-danger);
|
||||
}
|
||||
|
||||
.detail-warning-text {
|
||||
color: var(--plugin-status-warn);
|
||||
}
|
||||
|
||||
.detail-status-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -924,11 +915,6 @@ body.pane-resizing {
|
||||
color: var(--plugin-source-subscribed-text);
|
||||
}
|
||||
|
||||
.plugin-source-badge.source-orphaned {
|
||||
background: var(--plugin-status-warn-bg);
|
||||
color: var(--plugin-status-warn);
|
||||
}
|
||||
|
||||
.plugin-cloud-link {
|
||||
color: var(--plugin-link-text);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace Slic3r
|
||||
// IMPORTANT: ordinal order is the Plugins dialog Source sort priority.
|
||||
Mine,
|
||||
Subscribed,
|
||||
Orphaned,
|
||||
Local
|
||||
};
|
||||
|
||||
@@ -21,7 +20,6 @@ namespace Slic3r
|
||||
{
|
||||
case PluginSource::Mine: return "mine";
|
||||
case PluginSource::Subscribed: return "subscribed";
|
||||
case PluginSource::Orphaned: return "orphaned";
|
||||
case PluginSource::Local: return "local";
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -103,7 +103,6 @@ struct PluginDialogItem
|
||||
bool loading = false;
|
||||
|
||||
bool is_cloud_plugin = false;
|
||||
bool orphaned = false;
|
||||
bool has_local_package = false;
|
||||
bool unauthorized = false;
|
||||
bool has_script_capability = false;
|
||||
@@ -244,7 +243,6 @@ nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
|
||||
payload_item["sharing_token"] = dialog_item.sharing_token;
|
||||
payload_item["thumbnail_url"] = dialog_item.thumbnail_url;
|
||||
payload_item["installed"] = dialog_item.has_local_package;
|
||||
payload_item["orphaned"] = dialog_item.orphaned;
|
||||
payload_item["installed_version"] = dialog_item.installed_version;
|
||||
payload_item["latest_version"] = dialog_item.latest_version;
|
||||
return payload_item;
|
||||
@@ -267,8 +265,7 @@ PluginSource derive_plugin_source(const PluginDescriptor& descriptor)
|
||||
const bool is_cloud = descriptor.is_cloud_plugin();
|
||||
const bool is_mine = is_cloud && has_cloud_meta && descriptor.cloud->is_mine;
|
||||
|
||||
if (is_cloud && has_cloud_meta && descriptor.cloud->orphaned)
|
||||
return PluginSource::Orphaned;
|
||||
// Source is ownership/locality only; issue states never replace this badge.
|
||||
if (is_mine)
|
||||
return PluginSource::Mine;
|
||||
if (is_cloud)
|
||||
@@ -281,33 +278,34 @@ PluginAvailableActions evaluate_action_policy(const PluginDialogItem& item)
|
||||
PluginAvailableActions available_actions;
|
||||
const bool is_loading = item.status == PluginStatus::Loading;
|
||||
const bool is_cloud = item.is_cloud_plugin;
|
||||
const bool is_orphaned = item.orphaned;
|
||||
const bool is_mine = item.source == PluginSource::Mine;
|
||||
const bool has_local = item.has_local_package;
|
||||
const bool authorized_for_install = !item.unauthorized;
|
||||
|
||||
available_actions.toggle_installs_cloud_plugin = is_cloud && !is_orphaned && !has_local && authorized_for_install;
|
||||
available_actions.toggle_installs_cloud_plugin = is_cloud && !has_local && authorized_for_install;
|
||||
available_actions.can_toggle = !is_loading && (has_local || available_actions.toggle_installs_cloud_plugin);
|
||||
|
||||
auto add_action = [&available_actions](const char* id, const char* label, bool enabled = true, bool danger = false) {
|
||||
available_actions.context_actions.push_back(PluginContextAction{id, label, enabled, danger});
|
||||
};
|
||||
|
||||
if (is_cloud && !is_orphaned) {
|
||||
if (is_cloud) {
|
||||
if (is_mine)
|
||||
add_action("delete_mine_plugin", "Delete", true, true);
|
||||
else
|
||||
add_action("unsubscribe_plugin", "Unsubscribe", true, true);
|
||||
} else if (is_orphaned && has_local) {
|
||||
add_action("delete_plugin", "Delete", true, true);
|
||||
} else if (has_local) {
|
||||
add_action("delete_plugin", "Delete", true, true);
|
||||
}
|
||||
|
||||
add_action("open_folder", "Show in folder", has_local);
|
||||
|
||||
if (!is_orphaned)
|
||||
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;
|
||||
}
|
||||
@@ -360,7 +358,6 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
|
||||
item.error_text = descriptor.normalized_error();
|
||||
item.has_error = descriptor.has_error();
|
||||
item.is_cloud_plugin = descriptor.is_cloud_plugin();
|
||||
item.orphaned = descriptor.cloud.has_value() && descriptor.cloud->orphaned;
|
||||
item.has_local_package = descriptor.has_local_package();
|
||||
item.unauthorized = descriptor.is_unauthorized();
|
||||
item.is_loaded = manager.is_plugin_loaded(descriptor.plugin_key);
|
||||
@@ -747,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1131,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;
|
||||
@@ -1140,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(){";
|
||||
|
||||
@@ -20,7 +20,6 @@ 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
|
||||
@@ -55,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.
|
||||
@@ -67,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.
|
||||
@@ -103,8 +106,6 @@ 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)
|
||||
@@ -116,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);
|
||||
}
|
||||
|
||||
@@ -1420,8 +1422,7 @@ 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{};
|
||||
const bool cloud_fetch_succeeded = m_cloud_service.fetch_manifests_into_descriptors(cloud_list, not_found, unauthorized);
|
||||
if (!cloud_fetch_succeeded) {
|
||||
if (!m_cloud_service.fetch_manifests_into_descriptors(cloud_list, not_found, unauthorized)) {
|
||||
if (wxTheApp != nullptr) {
|
||||
GUI::wxGetApp().CallAfter([] {
|
||||
if (GUI::wxGetApp().is_closing())
|
||||
@@ -1436,10 +1437,9 @@ void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_f
|
||||
}
|
||||
}
|
||||
|
||||
if (cloud_fetch_succeeded)
|
||||
update_cloud_metadata(cloud_list);
|
||||
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.
|
||||
@@ -1448,28 +1448,19 @@ 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();
|
||||
}
|
||||
|
||||
// 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 : 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;
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::string& uuid : unauthorized) {
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
@@ -49,22 +48,33 @@ const char* const CLOUD_PLUGIN_SOURCE = R"PY(# /// script
|
||||
# version = "1.0"
|
||||
# ///
|
||||
print('ok')
|
||||
|
||||
import orca
|
||||
class stubscript(orca.script.ScriptPluginCapabilityBase):
|
||||
def get_name(self):
|
||||
return "stubscript"
|
||||
def execute(self):
|
||||
return orca.ExecutionResult.success("Stub orca script.")
|
||||
|
||||
@orca.plugin
|
||||
class stubpackage(orca.base):
|
||||
def register_capabilities(self):
|
||||
orca.register_capability(stubscript)
|
||||
)PY";
|
||||
|
||||
} // 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
|
||||
@@ -142,31 +152,4 @@ TEST_CASE("cloud metadata refresh preserves a plugin's stored config", "[PluginC
|
||||
reloaded.load();
|
||||
REQUIRE(reloaded.has_config(id));
|
||||
CHECK(reloaded.get_config(id)->config == configured);
|
||||
|
||||
// A local package can remain after the cloud subscription disappears. The cloud identity is
|
||||
// retained for diagnosis, but the orphaned state must suppress update availability until the
|
||||
// plugin is returned by a later cloud refresh.
|
||||
PluginDescriptor orphaned_record = cloud_record;
|
||||
orphaned_record.cloud->orphaned = true;
|
||||
orphaned_record.cloud->update_available = true;
|
||||
manager.update_cloud_metadata({orphaned_record});
|
||||
|
||||
const PluginDescriptor orphaned = find_by_uuid();
|
||||
REQUIRE(orphaned.cloud.has_value());
|
||||
CHECK(orphaned.cloud->orphaned);
|
||||
CHECK_FALSE(orphaned.has_error());
|
||||
CHECK(orphaned.get_update_status() == PluginUpdateStatus::Normal);
|
||||
|
||||
// Orphaned is informational only: the local package must remain loadable and usable.
|
||||
std::string load_error;
|
||||
manager.load_plugin(uuid, /*skip_deps=*/true);
|
||||
REQUIRE(manager.wait_for_plugin_load(uuid, std::chrono::seconds(120), load_error));
|
||||
INFO("load error: " << load_error);
|
||||
CHECK(load_error.empty());
|
||||
CHECK(manager.is_plugin_loaded(uuid));
|
||||
CHECK(manager.unload_plugin(uuid));
|
||||
|
||||
// Seeing the plugin in a subsequent cloud response clears the orphaned marker.
|
||||
manager.update_cloud_metadata({cloud_record});
|
||||
CHECK_FALSE(find_by_uuid().cloud->orphaned);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user