Files
OrcaSlicer/src/slic3r/GUI/PluginsDialog.cpp
T
a5413efc37 refactor: printer agent infrastructure changes to work with other printer agents aside from bambu (#15710)
* Add developer flag for printer agents

* Parse user print info on the UI thread to prevent heap corruption (#119)

get_user_print_info()'s HTTP fetch can run on a worker thread (e.g. BindJob),
but parse_user_print_info() mutates userMachineList (insert/erase/delete
MachineObject). on_machine_alive (SSDP) mutates the same maps on the UI thread
without locking, so parsing off-thread races the map and frees MachineObjects
out from under it -> heap corruption.

Keep all device-list mutation on the UI thread: parse inline when already on
the main thread, otherwise marshal via CallAfter so it stays serialized with
on_machine_alive.

* Prevent loss of user access code on LAN reselect

Keep user access code intact to maintain access rights even if
device slot is unpopulated, ensuring continuous connection
and status message reception.

* Harden send flow and separate upload failure recovery (#111)

* fix(send): harden FT send path + IP pre-flight UX

* Remove early returns

* Working Moonraker and Qidi printer agent transport (#104)

Folds the Qidi AMS box-mapping print
overrides (apply_box_mapping +
start_* wrappers) that the transport
fix builds on.

* Add support for runtime error status in plugins

Distinguish a loaded plugin whose
capability errored (RuntimeError,
warn-styled, stays checked) from a
load-time Error. Status now derives
via resolve_plugin_status(); enum
ordinal keeps dialog sort priority.
Unloading clears stale errors.

* Resolve duplicate agent ID conflicts

Reject a printer-agent capability
whose agent ID is already owned by
another capability or built-in:
flag the plugin error, disable the
capability, and warn the user
instead of silently ignoring it.

* fix: checkbox should depend on plugin is_loaded status

* Replace fake-enum printer agent dropdown (#121)

A dedicated PrinterAgentChoice field
reads rows straight from the live
agent registry and stores the agent
id string, replacing the fake-coEnum
index mapping. The field moves to
TabPrinter and registers with the
searcher so UnsavedChanges renders
it; the PhysicalPrinterDialog copy
and its update hook are removed
(#125). switch_printer_agent now
resolves ids via
resolve_printer_agent_id.

* Reset device selection on agent swap or unload (#124)

set_live_printer_agent centralizes
the swap: deselect the machine,
clear stale sidebar state and the
previous agent's Other Devices, then
install the new agent (or null when
its provider vanished). Plugin
load/unload callbacks refresh the
dropdown and re-run agent selection.
load_last_machine no longer falls
back to the first available machine.

* Gate agent mode behind use_printer_agents toggle

Replace per-printer auto-activation
(is_current_printer_agent_plugin)
with a global experimental AppConfig
toggle, default off: legacy
print-host behavior is unchanged
until the user opts in. The toggle
drives device-tab routing, print
button defaults, connect-button
visibility and sidebar layout, and
dedups machine-select dialog opens.

* Track BBLPrinterAgentPlugin.py

* Add printer-agent and plugin status tests

Ports the agent lifecycle, duplicate
agent-id, built-in-id clash and
status-resolution tests. The loader
runs on a detached worker thread, so
the lifecycle tests live in their own
executable. Tests install the
production unload-side registry
wiring themselves (no GUI in the
test binary) and register agents
manually so concurrent loads stay
deterministic.

* fix: pin HTTP to prevent connection refusal

Set `use_ssl` to false to ensure Moonraker
connectivity, as the service uses HTTP rather
than HTTPS, preventing connection issues. Initialize
device info early for reliable name resolution.

* Bring Moonraker device panel to feature parity

The monitor panel showed wrong or missing
data for Moonraker printers, and its
controls did nothing.

Push payload now carries layer number and
total layers. Remaining time replaces the
wrong total_duration - print_duration
formula. The chamber light toggle maps to
Klipper SET_PIN / SET_LED, and pause,
resume and stop post to
/printer/print/{action}. Task thumbnails
resolve via /server/files/thumbnails onto
a new MachineObject thumbnail url.

Filament sync switches to pull mode so the
agent is queried on demand.

Not compiled or run.

* Stop blocking print on unreported nozzle data

* fix: make Klipper macro lamp control reliable

* Surface Moonraker webcams and gate unrunnable controls

* Keep Bambu AMS dialect out of the agent waist

M620 is Bambu firmware dialect, not a
neutral command. Composing it in
MachineObject let non-Bambu agents
(Moonraker/Klipper) forward it and
report success on firmware that
cannot run it.

Agents now own the dialect: the
default refusal on IPrinterAgent
returns not-supported so the UI
can say so; BBLPrinterAgent keeps
the byte-identical composition.

* Fix multi-color filament logic

Reuse color decoding across functions to improve
code readability and maintain consistency in
multi-color filament handling.

* Move Moonraker commands off the UI thread

Pause/resume/stop, g-code sends, temps, and
light ran synchronous HTTP on the UI thread,
freezing the app up to 10s per click on slow
or unreachable printers.

Run them on a single agent-owned FIFO worker
so g-code ordering is preserved, while command
translation stays synchronous so unsupported-
command dialogs still work.

Add a pending-disabled state to the pause,
resume, and abort buttons for Moonraker-family
printers: the icon only flips once the
WebSocket reports the real state, which also
rules out double-click races.

* Show Snapmaker U1 camera in Device tab

The U1 exposes no /server/webcams/list entry;
its camera only captures after an explicit
camera.start_monitor RPC, which the Moonraker
websocket executes unauthenticated but only
answers over MQTT - so the call is fire and
forget.

Start the camera when the camera view is
shown and renew every 300 s: the printer
retires the capture task at ~362 s and
stop_monitor is accepted but ineffective,
so teardown is simply to stop renewing.

Frames land in monitor.jpg as still JPEGs
(~2 fps at interval 0), so the webview loads
a local HTML wrapper that repolls with a
cache buster.

* fix: start stream when camera URL changes

* fix: stop Qidi slot parse throwing on null

* Keep printer-agent progress in sync

Keep the shared task progress aligned with agent
reports that lack Bambu cloud task identity.

Release the lazily allocated task during reset to
avoid leaks when machine objects reconnect.

* docs: document the printer-agent subsystem

* fix: merge access codes into one

* Reconcile implementation split with PR tip

* feat: abstract remaining gcode commands in devicemanager

* refactor: abstract bambu specific protocol to printer agent

* refactor: push bbl workflows to bbl printer agent

* remove unused

* fix: default impl

* fix callback error

* fix: remove redundant cache

* specify api for getting file transfer url

* revert file transfer abstraction

* fix: ams filament mapping workflow

* feat: update qidi to use subscription based filament sync mode

* fix: resolve stubgen byte header conflict

* fix: ams sync info and periodic ams sync via subscription workflow

* fix: skip filament sync dialog if filamentSyncMode is none

* feat: parse nozzle information for qidi and moonraker printer agents

* fix: extend access code requirements t 0, 8 or more characters.

* remove irrelevant docs

* fix: remove heavy includes from IPrinterAgent

* fix: defer filesystem and camera abstractions

* fix: remote do_fetch_filament_info from tests

* cleanup moonraker and snapmaker printer agents

* fix: access codes regression

* fix: tests

* fix: printer agent switching on preset change

* fix: remove unused variable

* fix: snapmaker U1 SelectMachineDialog blocking print

* fix: merge artifact

* fix: clear up some unrelated changes

* feat: connect to cloud printer and monitor

* feat: connect to cloud printer and monitor

* feat: generic camera stream support for http snapshot and rtsp

* fix: build & access code UI

* feat: generic camera stream support for http snapshot and rtsp

* fix: build & access code UI

* feat: connect to cloud printer and monitor

* feat: connect to cloud printer and monitor

* fix: build errors

* feat: camera via webrtc

* fix: build

* fix: cmake

* feat: remove frame assembler and change config to set protocol

* fix: orcaprinteragent refactor

* fix: LAN paths and camera stream

* feat: use ffmpeg to render http camera stream

* fix: make model_id/dev_type optional instead of blocking

* fix: connect via ip dialog

* feat: LAN impl for Orca Printer Agent

* fix: model_id resolution method for non bambu printers

* fix: ffmpeg http camera stream jittering due to incomplete frames

* fix: revert sdcard check

* feat: check printer storage status before sending

* fix: moonraker printer agent hang on printer power cut

* fix: shim layer for any compatibiliity changes

* fix: cloud printers were using the wrong MQTT endpoint

* feat: cloud download via HTTP

* temp: doc for intended change

* fix: warnings

* fix: warnings

* fix: camera auto-play on startup

* fix: split infra from impl

* fix: uninitialized ams state blocking print

* Fixes nullptr deref

* Log first before std::move

* fix: printer agent virutal optional functions

* fix: parameterize orcaslicer_copy_test_dlls() for printer_agent_plugin_tests

* Revert "fix: parameterize orcaslicer_copy_test_dlls() for printer_agent_plugin_tests"

This reverts commit 2f566e3779.

* Guard libdatachannel. Remove unused code

* fix: unit tests & unused variables

* fix(ci): deps build order for datachannel

* Resolve printer agent first before getting cloud printer agent

* fix(ci): set depends openssl

* fix: re-include apply header guarded by ifdef __APPLE__

* fix(ci): add libdatachannel to flatpak manifest

* fix: add internal_developer_mode chekc back to MediaPlayCtrl::load()

* fix: invoke js clearInterval on WebMediaController::stop

* fix: change rtc log level

* fix: inject provider, agent id and generation to get_user_print_info to ensure correct metadata

* fix: revert moonraker specific behavior

* fix: remove stale comment

* fix: use ORCA_CLOUD_PROVIDER instead of hardcoded string

* fix: remove hardcoded ICE servers

* feat: enable https camera stream mode

* fix: move non-mandatory printer agent function stubs to IPrinterAgent

* fix: dedupe compatible printer type check

* fix: stop the correct media controller

* fix: scope get_my_machine_list to printers listed under the current printer agent

* fix: move printer agent plugin tests into test_plugin_lifecycle.cpp

* fix: always build bundled DataChannel dep

* fix: disable unused DataChannel media support

* revert: filament sync work

* fix: wrap command_* with small wrapper

* fix: regression bug, connecting to bambu needs bblp username

* fix: default impl for vendor agnostic gcode commansd

* refactor: media controller playback routing and ownership

* fix: bump libdatachannel ver & update flatpak to use tar instead

* fix: stop flatpak DataChannel build from re-cloning over the sandboxed network

* fix: update windows ffmpeg prebuild

* fix: update printer agent plugin API

* fix: shift camera signaling channel to network agent

* fix: follow external-packages for flatpak libdatachannel deps & add flatpak path to use source_dir

* feat: add printer-agent.md doc to HLSD

* fix: guard DeviceManager command dispatch when no printer agent is bound

* fix: port BBL implementations from #15711

* refactor: connect_printer api and dialog

* fix(tests): make omitted printer agent operations answer like a missing agent

* fix: preserve printer agent defaults in PrinterAgentPluginCapabilityTrampoline

* test: cover printer agent default command dispatch

* fix: validate windows FFmpeg avformat library

* feat: extend optional printer model warnings to calibration & ams workflows

* fix: handle malformed printer progress values safely

* fix: clear webview document on stop

* chore: reduce diagnostic logging level to trace

* refactor: centralize printer compatibility checks

* tests: add device manager integration coverage

* tests: cover WebMediaController lifecycle with wxWebView stub

* fix: make integration tests headless

* refactor: collapse command_ams_refresh_rfid and command_ams_refresh_rfid2

* refactor: make printer connection SSL agent-specific

* fix: persist input printer host and port

* fix: use correct device id for Moonraker connections

* fix: make moonraker gcode commands asynchronous

* fix: preserve moonraker device names

* fix: add include for non BBL_RELEASE_TO_PUBLIC path in BBLPrinterAgent

---------

Co-authored-by: Andrew <159703254+andrewsoonqn@users.noreply.github.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
Co-authored-by: Lam Wei Lun <weilun.lam@gmail.com>
2026-10-01 00:30:44 +08:00

1355 lines
56 KiB
C++

#include "PluginsDialog.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "I18N.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "slic3r/plugin/PluginConfig.hpp"
#include "slic3r/plugin/PluginFsUtils.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include <libslic3r/Utils.hpp>
#include <slic3r/GUI/NotificationManager.hpp>
#include <slic3r/GUI/Plater.hpp>
#include <slic3r/GUI/format.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <vector>
#include <wx/dialog.h>
#include <wx/event.h>
#include <wx/filedlg.h>
#include <wx/msgdlg.h>
#include <wx/progdlg.h>
#include <wx/timer.h>
#include <wx/utils.h>
namespace Slic3r { namespace GUI {
namespace {
const wxString kDeletePluginTitle = _L("Delete Plugin");
const wxString kUnsubscribeTitle = _L("Unsubscribe");
const wxString kOverwritePluginTitle = _L("Overwrite Plugin");
std::string s_selected_plugin_install_action = "explore";
struct PluginContextAction
{
std::string id;
std::string label;
bool enabled = true;
bool danger = false;
};
struct PluginAvailableActions
{
bool can_toggle = false;
bool toggle_installs_cloud_plugin = false;
std::vector<PluginContextAction> context_actions;
};
struct PluginCapabilityView
{
PluginCapabilityId id;
bool enabled = false;
bool can_toggle = false;
bool can_run = false;
// Whether the capability supplies its own config UI; every capability is configurable, this only
// picks the editor. False for descriptor-only rows: an unloaded plugin has no capabilities yet.
bool has_config_ui = false;
};
struct PluginChangelogView
{
std::string version;
std::string changelog;
long long created_time = 0;
};
// View-model for one plugin row in the dialog
struct PluginDialogItem
{
std::string plugin_key;
std::string plugin_id;
std::string display_name;
std::string description;
std::string author;
std::string version;
std::string installed_version;
std::string latest_version;
std::string sort_version; // Version shown in the row (installed if installed, else latest); used by the Version sort.
std::string type_label;
std::string type_key;
std::string sharing_token;
std::string thumbnail_url;
std::vector<std::string> type_labels;
std::vector<PluginChangelogView> changelog;
PluginSource source = PluginSource::Local;
PluginStatus status = PluginStatus::Inactive;
PluginUpdateStatus update_status = PluginUpdateStatus::Normal;
std::string error_text;
bool has_error = false;
bool is_loaded = false;
bool loading = false;
bool is_cloud_plugin = false;
bool orphaned = false;
bool has_local_package = false;
bool unauthorized = false;
bool has_script_capability = false;
bool can_run_script = false;
// Runtime capabilities in registration order, or descriptor-only type rows when unloaded.
std::vector<PluginCapabilityView> capabilities;
PluginAvailableActions available_actions;
};
constexpr bool kFetchCloudMeta = true;
constexpr bool kUseCurrentCloudMeta = false;
// The type of a package's first loaded capability. A package that is not loaded has no type.
PluginCapabilityType primary_capability_type_of(PluginManager& manager, const std::string& plugin_key)
{
const auto capabilities = manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Unknown, /*only_enabled=*/false);
return capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type();
}
PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
{
descriptor.plugin_root.clear();
descriptor.entry_path.clear();
descriptor.installed_version.clear();
// No local package is left behind, so the package verdict and any load error it produced go with it.
descriptor.metadata_valid = false;
descriptor.clear_error();
if (descriptor.cloud.has_value()) {
descriptor.cloud->installed = false;
descriptor.cloud->update_available = false;
}
return descriptor;
}
std::string to_string(PluginUpdateStatus status);
nlohmann::json build_context_actions_payload(const PluginAvailableActions& available_actions);
nlohmann::json build_plugin_payload_item(const PluginDialogItem& dialog_item)
{
nlohmann::json payload_item;
payload_item["plugin_key"] = dialog_item.plugin_key;
payload_item["plugin_id"] = dialog_item.plugin_id;
payload_item["name"] = dialog_item.display_name;
payload_item["description"] = dialog_item.description;
payload_item["author"] = dialog_item.author;
payload_item["version"] = dialog_item.version;
payload_item["type"] = dialog_item.type_label;
payload_item["type_key"] = dialog_item.type_key;
payload_item["types"] = dialog_item.type_labels;
nlohmann::json caps = nlohmann::json::array();
for (const PluginCapabilityView& capability : dialog_item.capabilities) {
nlohmann::json c;
c["name"] = capability.id.name;
c["type"] = plugin_capability_type_display_name(capability.id.type);
c["type_key"] = plugin_capability_type_to_string(capability.id.type);
c["enabled"] = capability.enabled;
c["can_toggle"] = capability.can_toggle;
c["can_run"] = capability.can_run;
c["has_config_ui"] = capability.has_config_ui;
caps.push_back(std::move(c));
}
payload_item["capabilities"] = std::move(caps);
// The Config tab's sidebar, built by the shared builder so it stays identical to PluginsConfigDialog's.
// Not the `capabilities` array above: that is the list tab's, with enable/run state and descriptor-only
// rows the config view has no use for.
std::vector<PluginCapabilityId> config_ids;
for (const PluginCapabilityView& capability : dialog_item.capabilities)
if (!capability.id.name.empty())
config_ids.push_back(capability.id);
payload_item["config_capabilities"] = PluginConfig::capabilities_payload(config_ids);
nlohmann::json changelog = nlohmann::json::array();
for (const PluginChangelogView& entry : dialog_item.changelog) {
nlohmann::json c;
c["version"] = entry.version;
c["changelog"] = entry.changelog;
c["created_time"] = entry.created_time;
changelog.push_back(std::move(c));
}
payload_item["changelog"] = std::move(changelog);
payload_item["label"] = dialog_item.display_name;
payload_item["source"] = to_string(dialog_item.source);
payload_item["status"] = to_string(dialog_item.status);
payload_item["is_loaded"] = dialog_item.is_loaded;
payload_item["error"] = dialog_item.error_text;
payload_item["update_status"] = to_string(dialog_item.update_status);
payload_item["unauthorized"] = dialog_item.unauthorized;
payload_item["context_actions"] = build_context_actions_payload(dialog_item.available_actions);
payload_item["update_available"] = dialog_item.update_status == PluginUpdateStatus::UpdateAvailable;
payload_item["can_toggle"] = dialog_item.available_actions.can_toggle;
payload_item["has_script_capability"] = dialog_item.has_script_capability;
payload_item["can_run_script"] = dialog_item.can_run_script;
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;
}
std::string to_string(PluginUpdateStatus status)
{
switch (status) {
case PluginUpdateStatus::Normal: return "normal";
case PluginUpdateStatus::UpdateAvailable: return "update_available";
case PluginUpdateStatus::Unauthorized: return "unauthorized";
}
return "normal";
}
PluginSource derive_plugin_source(const PluginDescriptor& descriptor)
{
const bool has_cloud_meta = descriptor.cloud.has_value();
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;
if (is_mine)
return PluginSource::Mine;
if (is_cloud)
return PluginSource::Subscribed;
return PluginSource::Local;
}
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.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});
};
// Owned cloud plugins fall through to the local delete: it removes the installed package only.
// Deleting a plugin from the cloud is a plugin hub operation and is never offered here.
if (is_cloud && !is_orphaned && !is_mine) {
add_action("unsubscribe_plugin", "Unsubscribe", 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;
}
PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
{
PluginDialogItem item;
PluginManager& manager = PluginManager::instance();
item.plugin_key = descriptor.plugin_key;
item.display_name = descriptor.name;
item.description = !descriptor.is_metadata_valid() && descriptor.has_error() ?
descriptor.normalized_error() :
(descriptor.description.empty() ? "No description." : descriptor.description);
item.author = descriptor.author;
item.version = descriptor.version;
// The cloud merge overwrites `version` with the latest cloud version, so prefer the preserved local
// one, falling back to `version` for local-only / pre-merge descriptors.
item.installed_version = descriptor.has_local_package() ?
(descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version) :
std::string{};
item.latest_version = descriptor.latest_available_version();
item.sort_version = item.installed_version.empty() ? item.latest_version : item.installed_version;
// A package that is not loaded has no capabilities, so its row contributes none and does not
// expand.
const std::vector<std::shared_ptr<PluginCapabilityInterface>> capabilities =
manager.get_plugin_capabilities(descriptor.plugin_key, PluginCapabilityType::Unknown, /*only_enabled=*/false);
const PluginCapabilityType primary_type = capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type();
item.type_label = plugin_capability_type_to_string(primary_type);
item.type_key = plugin_capability_type_to_string(primary_type);
// "types" is the display-only compatibility list. Cloud plugins show the raw labels the
// service returned (which may not map to real capability types); local plugins derive them
// from the capabilities actually loaded.
if (descriptor.is_cloud_plugin() && !descriptor.display_types.empty()) {
for (const std::string& label : descriptor.display_types)
if (std::find(item.type_labels.begin(), item.type_labels.end(), label) == item.type_labels.end())
item.type_labels.push_back(label);
} else {
for (const auto& capability : capabilities) {
const std::string label = plugin_capability_type_display_name(capability->type());
if (std::find(item.type_labels.begin(), item.type_labels.end(), label) == item.type_labels.end())
item.type_labels.push_back(label);
}
}
for (const PluginChangelog& entry : descriptor.changelog)
item.changelog.push_back({entry.version, entry.changelog, entry.created_time});
item.source = derive_plugin_source(descriptor);
item.update_status = descriptor.get_update_status();
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);
item.loading = manager.is_plugin_load_in_progress(descriptor.plugin_key);
item.has_script_capability = false;
for (const auto& capability : capabilities) {
item.capabilities.push_back({capability->identity(), capability->is_enabled(), true, false, capability->config_ui_available()});
if (capability->type() == PluginCapabilityType::Script)
item.has_script_capability = true;
}
item.sharing_token = descriptor.sharing_token;
item.thumbnail_url = descriptor.thumbnail_url;
item.status = resolve_plugin_status(item.loading, item.has_error, item.is_loaded);
item.available_actions = evaluate_action_policy(item);
const bool has_enabled_script = std::any_of(item.capabilities.begin(), item.capabilities.end(),
[](const PluginCapabilityView& capability) {
return capability.id.type == PluginCapabilityType::Script && capability.enabled;
});
item.can_run_script = descriptor.is_metadata_valid() && !descriptor.has_error() && item.has_script_capability && item.is_loaded &&
!item.loading && has_enabled_script;
for (PluginCapabilityView& capability : item.capabilities) {
capability.can_run = item.can_run_script && capability.id.type == PluginCapabilityType::Script && capability.enabled;
}
return item;
}
const PluginContextAction* find_context_action(const PluginAvailableActions& available_actions, const std::string& action_id)
{
const auto it = std::find_if(available_actions.context_actions.begin(), available_actions.context_actions.end(),
[&action_id](const PluginContextAction& action) { return action.id == action_id; });
return it != available_actions.context_actions.end() ? &(*it) : nullptr;
}
nlohmann::json build_context_actions_payload(const PluginAvailableActions& available_actions)
{
nlohmann::json payload = nlohmann::json::array();
for (const PluginContextAction& action : available_actions.context_actions) {
nlohmann::json item;
item["id"] = action.id;
item["label"] = action.label;
item["enabled"] = action.enabled;
item["danger"] = action.danger;
payload.push_back(std::move(item));
}
return payload;
}
struct PluginOperationState
{
std::mutex mutex;
bool succeeded = false;
std::string error;
};
void store_plugin_operation_result(const std::shared_ptr<PluginOperationState>& state, bool succeeded, std::string error)
{
std::lock_guard<std::mutex> lock(state->mutex);
state->succeeded = succeeded;
state->error = std::move(error);
}
bool take_plugin_operation_result(const std::shared_ptr<PluginOperationState>& state, std::string& error)
{
std::lock_guard<std::mutex> lock(state->mutex);
error = std::move(state->error);
return state->succeeded;
}
} // namespace
// ── Dialog-independent plugin actions (also used by the speed dial) ───────────────────────────
namespace {
// Snapshot of the currently-known cloud plugin descriptors, used to refresh metadata without a
// network round-trip (kUseCurrentCloudMeta).
std::vector<PluginDescriptor> current_cloud_metadata_snapshot()
{
std::vector<PluginDescriptor> cloud_entries;
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
if (entry.is_cloud_plugin())
cloud_entries.push_back(entry);
return cloud_entries;
}
} // namespace
void refresh_plugin_metadata_blocking(bool fetch_cloud)
{
PluginManager& manager = PluginManager::instance();
std::vector<std::string> not_found, unauthorized;
const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} :
current_cloud_metadata_snapshot();
manager.rescan_plugins();
if (!fetch_cloud) {
manager.update_cloud_metadata(current_cloud_metadata);
return;
}
manager.fetch_plugins_from_cloud(&not_found, &unauthorized);
wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() {
if (wxGetApp().is_closing())
return;
Plater* plater = wxGetApp().plater();
if (plater == nullptr)
return;
for (const auto& uuid : not_found)
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
format(_L("Plugin %s is no longer available."), uuid));
for (const auto& uuid : unauthorized)
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
format(_L("Plugin %s access is unauthorized."), uuid));
});
}
void open_plugin_hub()
{
std::string cloud_base_url = "https://cloud.orcaslicer.com";
if (wxGetApp().getAgent()) {
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
if (orca_agent && !orca_agent->get_cloud_base_url().empty())
cloud_base_url = orca_agent->get_cloud_base_url();
}
while (!cloud_base_url.empty() && cloud_base_url.back() == '/')
cloud_base_url.pop_back();
if (cloud_base_url.empty())
cloud_base_url = "https://cloud.orcaslicer.com";
wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub"));
}
bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message)
{
message.clear();
if (package_file.empty())
return false;
// ---- pre-flight (main thread): validate + inspect + overwrite prompt ----
const wxString package_name = from_u8(package_file.filename().string());
std::string extension = package_file.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (extension != ".py" && extension != ".whl") {
message = _L("Select a .py or .whl plugin package.");
return false;
}
PluginDescriptor plugin_descriptor;
bool existing_installation = false;
std::string error;
try {
if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error)) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << error;
message = _L("Failed to install plugin package. See the log for details.");
return false;
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << ex.what();
message = _L("Failed to install plugin package. See the log for details.");
return false;
} catch (...) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file;
message = _L("Failed to install plugin package. See the log for details.");
return false;
}
if (existing_installation) {
const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
wxMessageDialog dialog(parent,
wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."),
plugin_name),
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
if (dialog.ShowModal() != wxID_OK) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installation cancelled before overwrite. package=" << package_file
<< " plugin=" << plugin_descriptor.name;
return false; // cancelled: message stays empty so callers stay silent
}
}
// ---- install + refresh on a worker behind a modal progress dialog (keeps the UI live) ----
bool installed = false;
{
struct Result
{
std::mutex mutex;
bool ok = false;
std::string error;
};
auto state = std::make_shared<Result>();
detail::run_wait_with_progress(
[state, package_file]() {
std::string error;
bool ok = false;
try {
ok = PluginManager::instance().install_plugin(package_file, error);
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
if (ok) {
// Reflect the new package in discovery/cloud metadata without blocking the caller.
try { refresh_plugin_metadata_blocking(kUseCurrentCloudMeta); } catch (...) {}
}
std::lock_guard<std::mutex> lock(state->mutex);
state->ok = ok;
state->error = std::move(error);
},
parent, _L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100,
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME, /*alive=*/nullptr, /*restore=*/{});
std::lock_guard<std::mutex> lock(state->mutex);
installed = state->ok;
error = std::move(state->error);
}
if (!installed) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package installation failed for " << package_file << " error=" << error;
message = _L("Failed to install plugin package. See the log for details.");
return false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installed successfully from " << package_file;
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
message = wxString::Format(_L("Installed \"%s\"."), installed_name);
return true;
}
PluginsDialog::PluginsDialog(wxWindow* parent, wxWindowID id, const wxString&, const wxPoint& pos, const wxSize& size, long style)
: WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style)
{ create_webview("web/dialog/PluginsDialog/index.html", _L("Plugins"), wxSize(900, 820), wxSize(760, 715)); }
PluginsDialog::~PluginsDialog() { m_alive->store(false, std::memory_order_release); }
void PluginsDialog::set_open_terminal_dlg_fn()
{
m_open_terminal_dlg_fn = [] { wxGetApp().open_terminal_dialog(); };
}
void PluginsDialog::update_plugin_dialog_ui()
{
// Called after the any metadata is already updated, for example from the
// cloud-plugin state callback. Do not fetch here or the callback can re-enter.
send_plugins();
resolve_pending_activation();
}
void PluginsDialog::resolve_pending_activation()
{
if (m_activating_plugin_key.empty())
return;
PluginManager& manager = PluginManager::instance();
if (manager.is_plugin_load_in_progress(m_activating_plugin_key))
return; // Still loading: keep the "Activating..." message until the load resolves.
const std::string plugin_key = m_activating_plugin_key;
m_activating_plugin_key.clear();
// Mirror the row's status precedence (Error before Activated) so the message matches the list.
PluginDescriptor descriptor;
if (get_descriptor(plugin_key, descriptor) && descriptor.has_error())
show_status(wxString::Format(_L("Failed to activate \"%s\"."), plugin_display_name(plugin_key)), "error");
else if (manager.is_plugin_loaded(plugin_key))
show_status(wxString::Format(_L("Activated \"%s\"."), plugin_display_name(plugin_key)), "success");
// Otherwise it ended up inactive (toggled off or cancelled mid-load): stay silent.
}
void PluginsDialog::on_script_message(const nlohmann::json& payload)
{
if (handle_common_script_command(payload))
return;
// Defer command handling out of the webview script-message callback: GTK and macOS
// deliver it synchronously inside the native webview callback (see ui_create_window
// in PluginHostUi.cpp), and window work on that stack is the crash class fixed in
// b779a7bfed/f2ccbfc8b5. Deferring here keeps every command handler in THIS dialog off
// that stack; it is not a guarantee for other WebViewHostDialog subclasses, which each
// have to defer for themselves (PluginsConfigDialog does; others still do not).
wxGetApp().CallAfter([this, alive = m_alive, payload]() {
if (alive->load(std::memory_order_acquire))
handle_web_command(payload);
});
}
void PluginsDialog::handle_web_command(const nlohmann::json& payload)
{
const std::string command = payload.value("command", "");
if (command == "request_plugins") {
send_plugins();
} else if (command == "refresh_plugins") {
refresh_plugins();
} else if (command == "toggle_plugin") {
toggle_plugin(payload.value("plugin_key", ""), payload.value("enabled", false));
} else if (command == "toggle_plugin_capability") {
toggle_plugin_capability(payload.value("plugin_key", ""), plugin_capability_type_from_string(payload.value("capability_type", "")),
payload.value("capability_name", ""), payload.value("enabled", false));
} else if (command == "install_local_plugin") {
install_plugin_from_file();
} else if (command == "plugin_menu_action") {
handle_plugin_menu_action(payload.value("plugin_key", ""), payload.value("action", ""));
} else if (command == "run_script_plugin_capability") {
run_script_plugin_capability(payload.value("plugin_key", ""), payload.value("capability_name", ""));
} else if (command == "open_terminal") {
m_open_terminal_dlg_fn();
} else if (command == "update_plugin") {
update_plugin(payload.value("plugin_key", ""));
} else if (command == "open_plugin_on_cloud") {
open_plugin_on_cloud(payload.value("sharing_token", ""));
} else if (command == "open_plugin_hub") {
open_plugin_hub();
} else if (command == "set_plugin_sort") {
set_plugin_sort(payload.value("sort_key", ""), payload.value("sort_order", ""));
} else if (command == "get_capability_config" || command == "save_capability_config" || command == "restore_capability_config") {
const PluginCapabilityId id{plugin_capability_type_from_string(payload.value("capability_type", "")),
payload.value("capability_name", ""), payload.value("plugin_key", "")};
if (command == "get_capability_config") {
send_capability_config(id);
return;
}
if (command == "restore_capability_config") {
restore_capability_config(id);
return;
}
// `config` is a JSON string from the default editor's textarea, or an already-structured
// value from a capability's custom UI. Both land here; save_capability_config sorts it out.
save_capability_config(id, payload.contains("config") ? payload.at("config") : nlohmann::json::object());
} else if (command == "set_plugin_install_action") {
const std::string action = payload.value("action", "");
if (action == "explore" || action == "install-local")
s_selected_plugin_install_action = action;
}
}
void PluginsDialog::send_plugins() { call_web_handler(build_plugins_payload()); }
void PluginsDialog::set_plugin_sort(const std::string& sort_key, const std::string& sort_order)
{
m_plugin_sort_key = plugin_sort_key_from_string(sort_key, m_plugin_sort_key);
m_plugin_sort_order = plugin_sort_order_from_string(sort_order, m_plugin_sort_order);
send_plugins();
}
nlohmann::json PluginsDialog::build_plugins_payload() const
{
nlohmann::json response;
response["command"] = "list_plugins";
response["install_action"] = s_selected_plugin_install_action;
response["sort_key"] = to_string(m_plugin_sort_key);
response["sort_order"] = to_string(m_plugin_sort_order);
response["data"] = nlohmann::json::array();
const auto rows = PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true);
BOOST_LOG_TRIVIAL(info) << "Prepared " << rows.size() << " plugin rows for Plugins dialog";
std::vector<PluginDialogItem> items;
items.reserve(rows.size());
for (const PluginDescriptor& row : rows)
items.push_back(build_plugin_dialog_item(row));
sort_plugin_items_for_dialog(items, m_plugin_sort_key, m_plugin_sort_order);
for (const PluginDialogItem& item : items)
response["data"].push_back(build_plugin_payload_item(item));
return response;
}
bool PluginsDialog::get_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const
{
PluginManager& manager = PluginManager::instance();
if (manager.try_get_valid_plugin_descriptor(plugin_key, descriptor))
return true;
return manager.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_invalid_package();
}
void PluginsDialog::refresh_plugin_metadata_async(const wxString& title, const wxString& message, bool fetch_cloud)
{
run_with_dialog([fetch_cloud]() { refresh_plugin_metadata_blocking(fetch_cloud); }, [this]() {
prompt_for_missing_plugins();
send_plugins();
}, title, message);
}
void PluginsDialog::prompt_for_missing_plugins()
{
PluginManager& manager = PluginManager::instance();
const std::vector<PluginDescriptor> missing = manager.get_missing_plugin_descriptors();
if (missing.empty())
return;
wxString names;
std::vector<std::string> keys;
keys.reserve(missing.size());
for (const PluginDescriptor& plugin : missing) {
keys.push_back(plugin.plugin_key);
names += "\n- ";
names += plugin_display_name(plugin.plugin_key);
}
const int result = wxMessageBox(
wxString::Format(_L("The following installed plugins were not found on disk:\n%s\n\nRemove them from OrcaSlicer?"), names),
_L("Missing Plugins"), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
restore_z_order();
if (result == wxYES)
manager.remove_missing_plugins(keys);
}
void PluginsDialog::refresh_plugins()
{
BOOST_LOG_TRIVIAL(info) << "Refreshing plugins from Plugins dialog";
refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kFetchCloudMeta);
}
void PluginsDialog::toggle_plugin(const std::string& plugin_key, bool enabled)
{
if (plugin_key.empty())
return;
BOOST_LOG_TRIVIAL(info) << "Toggle plugin request. plugin_key=" << plugin_key << " enabled=" << enabled;
PluginDescriptor row_data;
if (!get_descriptor(plugin_key, row_data)) {
send_plugins();
return;
}
PluginDialogItem dialog_item = build_plugin_dialog_item(row_data);
PluginAvailableActions available_actions = dialog_item.available_actions;
PluginManager& manager = PluginManager::instance();
if (!enabled) {
if (!manager.unload_plugin(plugin_key)) {
BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin from Plugins dialog: " << plugin_key;
show_status(_L("Failed to unload plugin."), "warn");
send_plugins();
return;
}
BOOST_LOG_TRIVIAL(info) << "Plugin unloaded from Plugins dialog: " << plugin_key;
// A user-disabled plugin has no meaningful error state.
if (!manager.clear_plugin_error(plugin_key))
BOOST_LOG_TRIVIAL(warning) << "Failed to clear plugin error for " << plugin_key << " (failed to find)";
// A prior activation of this plugin is moot now; drop it so no stale "Activated" arrives later.
if (m_activating_plugin_key == plugin_key)
m_activating_plugin_key.clear();
send_plugins();
show_status(wxString::Format(_L("Deactivated \"%s\"."), plugin_display_name(plugin_key)), "success");
return;
}
if (!available_actions.can_toggle) {
if (dialog_item.unauthorized && available_actions.toggle_installs_cloud_plugin == false && row_data.has_local_package() == false) {
const std::string install_error = "Unauthorized cloud plugins cannot be installed.";
manager.set_plugin_error(plugin_key, install_error);
show_status(from_u8(install_error), "warn");
}
send_plugins();
return;
}
if (available_actions.toggle_installs_cloud_plugin) {
if (!install_cloud_plugin(row_data.plugin_key, row_data.version, from_u8(row_data.name))) {
BOOST_LOG_TRIVIAL(warning) << "Cloud plugin install was not completed for " << plugin_key;
send_plugins();
return;
}
BOOST_LOG_TRIVIAL(info) << "Cloud plugin installed locally from Plugins dialog: " << plugin_key;
// download_and_install_cloud_plugin already updated the descriptor, so no rescan is needed here.
if (!get_descriptor(plugin_key, row_data)) {
send_plugins();
return;
}
dialog_item = build_plugin_dialog_item(row_data);
if (dialog_item.has_error) {
send_plugins();
return;
}
}
if (manager.is_plugin_loaded(plugin_key)) {
send_plugins();
return;
}
PluginDescriptor valid_descriptor;
if (!manager.try_get_valid_plugin_descriptor(plugin_key, valid_descriptor)) {
show_status(_L("Plugin manifest was not found."), "warn");
send_plugins();
return;
}
if (manager.is_plugin_load_in_progress(plugin_key)) {
send_plugins();
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Starting plugin load from web dialog: " << plugin_key;
manager.load_plugin(plugin_key);
send_plugins();
// Loading is asynchronous: show a pending message now; resolve_pending_activation() turns it into
// Activated/Failed once the loader's completion callback runs update_plugin_dialog_ui().
m_activating_plugin_key = plugin_key;
show_status(wxString::Format(_L("Activating \"%s\"..."), plugin_display_name(plugin_key)), "info");
}
void PluginsDialog::toggle_plugin_capability(const std::string& plugin_key,
PluginCapabilityType type,
const std::string& capability_name,
bool enabled)
{
if (plugin_key.empty() || capability_name.empty() || type == PluginCapabilityType::Unknown)
return;
PluginDescriptor row_data;
if (!get_descriptor(plugin_key, row_data)) {
send_plugins();
return;
}
if (!PluginManager::instance().get_plugin_capability({type, capability_name, plugin_key}, /*only_enabled=*/false)) {
BOOST_LOG_TRIVIAL(warning) << "Cannot toggle missing plugin capability: " << plugin_key << " | " << capability_name;
send_plugins();
return;
}
PluginManager& manager = PluginManager::instance();
if (enabled) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Enabling plugin capability: " << plugin_key << " | " << capability_name;
manager.set_capability_enabled({type, capability_name, plugin_key}, true);
} else {
// check if the capability is currently in use here.
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Disabling plugin capability: " << plugin_key << " | " << capability_name;
manager.set_capability_enabled({type, capability_name, plugin_key}, false);
}
send_plugins();
show_status(wxString::Format(enabled ? _L("Enabled \"%s\".") : _L("Disabled \"%s\"."), from_u8(capability_name)), "success");
}
void PluginsDialog::handle_plugin_menu_action(const std::string& plugin_key, const std::string& action)
{
PluginDescriptor row_data;
if (!get_descriptor(plugin_key, row_data))
return;
const PluginDialogItem dialog_item = build_plugin_dialog_item(row_data);
const PluginContextAction* context_action = find_context_action(dialog_item.available_actions, action);
if (!context_action || !context_action->enabled)
return;
if (action == "open_folder") {
open_plugin_folder(row_data);
} else if (action == "delete_plugin") {
delete_local_plugin(row_data);
} else if (action == "unsubscribe_plugin") {
unsubscribe_cloud_plugin(row_data);
} 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);
}
}
void PluginsDialog::restore_z_order()
{
// Deferred so it runs on a clean stack after the modal has fully torn down; the
// alive guard covers the dialog being destroyed while the CallAfter is queued.
wxGetApp().CallAfter([this, alive = m_alive]() {
if (alive->load(std::memory_order_acquire) && IsShown())
Raise();
});
}
void PluginsDialog::install_plugin_from_file()
{
wxFileDialog dialog(this, _L("Select plugin package"), wxEmptyString, wxEmptyString, _L("Plugin files (*.py;*.whl)|*.py;*.whl"),
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
const int rc = dialog.ShowModal();
restore_z_order();
if (rc != wxID_OK)
return;
if (!install_plugin_package(dialog.GetPath().ToUTF8().data())) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to install plugin package.";
}
}
bool PluginsDialog::install_plugin_package(const std::string& package_path)
{
if (package_path.empty())
return false;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Installing local plugin package from path: " << package_path;
const boost::filesystem::path package_file(package_path);
wxString message;
const bool installed = install_local_plugin_package(package_file, this, message);
// The helper's overwrite prompt and progress dialog can push this webview behind; re-raise it
// once, after both have closed (the speed-dial path parents to the mainframe instead).
restore_z_order();
// The shared helper reports a user-cancelled overwrite with an empty message: stay silent.
if (message.IsEmpty()) {
send_plugins();
return false;
}
if (!installed) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to install plugin package.";
show_status(message, "warn");
send_plugins();
return false;
}
show_status(message, "success");
prompt_for_missing_plugins();
send_plugins();
return true;
}
bool PluginsDialog::install_cloud_plugin(const std::string& plugin_key, const std::string& version, const wxString& name)
{
if (plugin_key.empty())
return false;
BOOST_LOG_TRIVIAL(info) << "Downloading cloud plugin. plugin_key=" << plugin_key << " name=" << into_u8(name);
std::string error;
bool downloaded = false;
try {
downloaded = run_with_dialog_wait(
[plugin_key, &error, version]() {
return PluginManager::instance().download_and_install_cloud_plugin(plugin_key, version, error);
},
_L("Downloading plugin"), _L("Downloading") + ": " + name);
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
if (!downloaded) {
BOOST_LOG_TRIVIAL(error) << "Cloud plugin download failed. plugin_key=" << plugin_key << " error=" << error;
PluginManager::instance().set_plugin_error(plugin_key, error);
show_status(error.empty() ? _L("Plugin download failed.") : from_u8(error), "error");
return false;
}
BOOST_LOG_TRIVIAL(info) << "Cloud plugin downloaded successfully. plugin_key=" << plugin_key;
return true;
}
void PluginsDialog::show_status(const wxString& message, const char* level)
{
nlohmann::json payload;
payload["command"] = "status_message";
payload["level"] = level;
payload["message"] = into_u8(message);
call_web_handler(payload);
}
wxString PluginsDialog::plugin_display_name(const std::string& plugin_key) const
{
PluginDescriptor descriptor;
if (get_descriptor(plugin_key, descriptor) && !descriptor.name.empty())
return from_u8(descriptor.name);
return from_u8(plugin_key);
}
void PluginsDialog::send_capability_config(const PluginCapabilityId& id) { call_web_handler(PluginConfig::get_config_response(id)); }
void PluginsDialog::save_capability_config(const PluginCapabilityId& id, const nlohmann::json& config)
{
const nlohmann::json response = PluginConfig::save_config_response(id, config);
call_web_handler(response);
if (response.value("ok", false))
show_status(_L("Configuration saved."), "success");
}
void PluginsDialog::restore_capability_config(const PluginCapabilityId& id)
{
// Destructive, so confirm first. The confirmation stays here rather than in PluginConfig: it needs
// a parent window.
const int rc = wxMessageBox(wxString::Format(_L("Restore the default configuration for \"%s\"?\n\n"
"This discards the settings currently saved for this capability."),
from_u8(id.name)),
_L("Restore defaults"), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
if (rc != wxYES)
return;
const nlohmann::json response = PluginConfig::restore_config_response(id);
call_web_handler(response);
if (response.value("ok", false))
show_status(_L("Default configuration restored."), "success");
}
void PluginsDialog::run_script_plugin_capability(const std::string& plugin_key, const std::string& capability_name)
{
PluginManager& manager = PluginManager::instance();
std::string error;
ExecutionResult result;
auto complete_with_error = [this, &manager, &plugin_key](const std::string& plugin_error, const wxString& status_message) {
const std::string normalized_error = plugin_error.empty() ? "Script plugin failed." : plugin_error;
if (!manager.set_plugin_error(plugin_key, normalized_error))
BOOST_LOG_TRIVIAL(warning) << "Failed to record plugin error. plugin_key=" << plugin_key;
if (!manager.unload_plugin(plugin_key))
BOOST_LOG_TRIVIAL(error) << "Failed to unload plugin after script error. plugin_key=" << plugin_key;
send_plugins();
// The row already shows "Error" and the Diagnostics tab holds the full text, so report in the
// footer status bar instead of a modal box.
const wxString message = status_message.empty() ? from_u8(normalized_error) : status_message;
show_status(message, "error");
};
{
wxBusyCursor busy;
result = manager.run_script_capability(plugin_key, capability_name, error);
}
if (!error.empty()) {
complete_with_error(error, wxString());
return;
}
BOOST_LOG_TRIVIAL(info) << "Script plugin execution completed. plugin_key=" << plugin_key
<< " status=" << static_cast<int>(result.status) << " message=" << result.message << " data=" << result.data;
const bool failed = result.status == PluginResult::RecoverableError || result.status == PluginResult::FatalError || !error.empty();
if (failed) {
complete_with_error(result.message, wxString());
return;
}
manager.clear_plugin_error(plugin_key);
send_plugins();
const bool skipped = result.status == PluginResult::Skipped;
const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
const wxString message = result.message.empty() ? fallback : from_u8(result.message);
show_status(message, skipped ? "info" : "success");
}
void PluginsDialog::update_plugin(const std::string& plugin_key)
{
if (plugin_key.empty())
return;
BOOST_LOG_TRIVIAL(info) << "Updating cloud plugin. plugin_key=" << plugin_key;
PluginDescriptor descriptor;
const wxString name = get_descriptor(plugin_key, descriptor) ? from_u8(descriptor.name) : from_u8(plugin_key);
// update_cloud_plugin unloads the old plugin, deletes its local package and reinstalls the latest
// version; all of that is off-main-thread work, so run it on the worker behind a progress dialog.
std::string error;
bool updated = false;
try {
updated = run_with_dialog_wait([plugin_key, &error]() { return PluginManager::instance().update_cloud_plugin(plugin_key, error); },
_L("Updating plugin"), _L("Updating") + ": " + name);
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
if (!updated) {
BOOST_LOG_TRIVIAL(error) << "Cloud plugin update failed. plugin_key=" << plugin_key << " error=" << error;
show_status(error.empty() ? _L("Failed to update plugin.") : from_u8(error), "error");
send_plugins();
return;
}
// update_cloud_plugin already updated the in-memory descriptor, so a UI refresh is enough here.
send_plugins();
show_status(wxString::Format(_L("Updated \"%s\"."), name), "success");
}
void PluginsDialog::open_plugin_folder(const PluginDescriptor& plugin)
{
const boost::filesystem::path plugin_folder = resolve_plugin_root_from_descriptor(plugin);
if (plugin_folder.empty()) {
show_status(_L("Plugin folder could not be determined."), "warn");
return;
}
desktop_open_any_folder(plugin_folder.string());
}
void PluginsDialog::open_plugin_on_cloud(const std::string& sharing_token)
{
if (sharing_token.empty())
return;
if (!wxGetApp().getAgent())
return;
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
if (!orca_agent)
return;
wxLaunchDefaultBrowser(wxString::FromUTF8(orca_agent->get_cloud_base_url() + "/p/" + sharing_token));
}
void PluginsDialog::open_plugin_hub() { Slic3r::GUI::open_plugin_hub(); }
void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin)
{
const wxString plugin_name = from_u8(plugin.name);
const int rc = wxMessageBox(
wxString::Format(plugin.is_cloud_plugin() ?
_L("Delete plugin \"%s\"?\n\nThis removes the local plugin files. The plugin stays in the cloud "
"and can be reinstalled.") :
_L("Delete plugin \"%s\"?\n\nThis permanently removes the plugin folder."),
plugin_name),
kDeletePluginTitle, wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
restore_z_order();
if (rc != wxYES)
return;
auto state = std::make_shared<PluginOperationState>();
run_with_dialog(
[plugin_key = plugin.plugin_key, cloud_row = as_cloud_only_descriptor(plugin), state]() {
std::string error;
const bool succeeded = PluginManager::instance().delete_plugin(plugin_key, error);
// Keep the cloud row listed so the plugin stays reinstallable without waiting on a cloud fetch.
if (succeeded && cloud_row.is_cloud_plugin())
PluginManager::instance().update_cloud_metadata(std::vector<PluginDescriptor>{cloud_row});
store_plugin_operation_result(state, succeeded, std::move(error));
},
[this, state, plugin_name]() {
std::string error;
if (!take_plugin_operation_result(state, error)) {
show_status(error.empty() ? _L("Failed to delete plugin.") : from_u8(error), "error");
return;
}
send_plugins();
show_status(wxString::Format(_L("Deleted \"%s\"."), plugin_name), "success");
},
_L("Deleting plugin"), _L("Deleting plugin..."));
}
void PluginsDialog::unsubscribe_cloud_plugin(const PluginDescriptor& plugin)
{
const wxString plugin_name = from_u8(plugin.name);
const int rc = wxMessageBox(
wxString::Format(_L("Unsubscribe plugin \"%s\"?\n\nThis will stop tracking the plugin and delete any local plugin files."),
plugin_name),
kUnsubscribeTitle, wxYES_NO | wxNO_DEFAULT | wxICON_WARNING, this);
restore_z_order();
if (rc != wxYES)
return;
auto state = std::make_shared<PluginOperationState>();
run_with_dialog(
[plugin_key = plugin.plugin_key, state]() {
std::string error;
const bool succeeded = PluginManager::instance().delete_and_unsubscribe_cloud_plugin(plugin_key, error);
store_plugin_operation_result(state, succeeded, std::move(error));
},
[this, state, plugin_name]() {
std::string error;
if (!take_plugin_operation_result(state, error)) {
show_status(error.empty() ? _L("Failed to unsubscribe plugin.") : from_u8(error), "error");
return;
}
send_plugins();
show_status(wxString::Format(_L("Unsubscribed \"%s\"."), plugin_name), "success");
},
_L("Unsubscribing plugin"), _L("Deleting local files and unsubscribing plugin..."));
}
void PluginsDialog::reload_local_plugin(const std::string& plugin_key, bool clear_cache)
{
if (plugin_key.empty())
return;
const bool was_loaded = PluginManager::instance().is_plugin_loaded(plugin_key);
std::pair<bool, std::string> reload_result{false, ""};
try {
reload_result = run_with_dialog_wait(
[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))
return {false, error.empty() ? "Plugin failed to load." : error};
if (!was_loaded && !manager.unload_plugin(plugin_key))
return {false, "Plugin reloaded, but failed to restore the inactive state."};
return {true, {}};
},
_L("Reloading plugin"), _L("Reloading plugin"));
} catch (const std::exception& ex) {
reload_result = {false, ex.what()};
} catch (...) {
reload_result = {false, "Unknown plugin reload error."};
}
if (!reload_result.first) {
show_status(reload_result.second.empty() ? _L("Failed to reload plugin.") : from_u8(reload_result.second), "warn");
send_plugins();
return;
}
send_plugins();
show_status(wxString::Format(_L("Reloaded \"%s\"."), plugin_display_name(plugin_key)), "success");
}
void PluginsDialog::reinstall_cloud_plugin(const PluginDescriptor& plugin)
{
const std::string plugin_key = plugin.plugin_key;
if (plugin_key.empty())
return;
PluginManager& manager = PluginManager::instance();
const bool was_loaded = PluginManager::instance().is_plugin_loaded(plugin_key);
std::string error;
if (plugin.has_local_package()) {
if (!manager.delete_plugin(plugin_key, error)) {
show_status(error.empty() ? _L("Failed to delete plugin.") : from_u8(error), "error");
send_plugins();
return;
}
manager.update_cloud_metadata(std::vector<PluginDescriptor>{as_cloud_only_descriptor(plugin)});
}
if (!install_cloud_plugin(plugin_key, plugin.version, from_u8(plugin.name))) {
send_plugins();
return;
}
if (was_loaded) {
std::pair<bool, std::string> reload_result{false, ""};
try {
reload_result = run_with_dialog_wait(
[plugin_key]() -> std::pair<bool, std::string> {
PluginManager& manager = PluginManager::instance();
manager.load_plugin(plugin_key);
std::string error;
if (!manager.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error) || !manager.is_plugin_loaded(plugin_key))
return {false, error.empty() ? "Plugin failed to load." : error};
return {true, {}};
},
_L("Reloading plugin"), _L("Reloading plugin"));
} catch (const std::exception& ex) {
reload_result = {false, ex.what()};
} catch (...) {
reload_result = {false, "Unknown plugin reload error."};
}
if (!reload_result.first) {
show_status(reload_result.second.empty() ? _L("Failed to reload plugin.") : from_u8(reload_result.second), "warn");
send_plugins();
return;
}
}
send_plugins();
show_status(wxString::Format(_L("Reloaded \"%s\"."), plugin_display_name(plugin_key)), "success");
}
}} // namespace Slic3r::GUI