feat: Python Plugins

This commit is contained in:
Ian Chua committed 2026-07-02 17:49:36 +08:00
1 parent 395e070a0e
commit ecddf3d18f
183 files changed
+49955 -2120

No files matched your search

+63
View File
@@ -129,6 +129,23 @@ if (MINGW)
set_target_properties(OrcaSlicer PROPERTIES PREFIX "")
endif (MINGW)
# The GUI embeds the bundled shared libpython (libslic3r_gui -> pybind11::embed);
# the dep recipe stamps it with an @rpath id / plain SONAME, so the executable
# needs an rpath entry for each layout in the "Bundled Python/uv layout" map
# further down. Dead entries are skipped by the loader.
# OrcaSlicer_profile_validator links libslic3r only (no Python).
if (APPLE)
# A linker flag rather than the BUILD_RPATH property because the release
# flow uses the Xcode generator, which does not honor BUILD_RPATH.
target_link_options(OrcaSlicer PRIVATE "LINKER:-rpath,@executable_path/python/lib")
elseif (UNIX AND NOT WIN32)
set_target_properties(OrcaSlicer PROPERTIES
# Build tree + AppImage, which packages the build-tree binary.
BUILD_RPATH "$ORIGIN/python/lib;$ORIGIN/../lib/python/lib"
# Flatpak (cmake --install).
INSTALL_RPATH "$ORIGIN/../libpython/lib")
endif ()
if (NOT WIN32 AND NOT APPLE)
# Binary name on unix like systems (Linux, Unix)
set_target_properties(OrcaSlicer PROPERTIES OUTPUT_NAME "orca-slicer")
@@ -194,6 +211,26 @@ endif ()
set(output_dlls_Release "")
set(output_dlls_Debug "")
set(output_dlls_RelWithDebInfo "")
# ---- Bundled Python/uv layout (canonical map) ---------------------------
# The runtime and uv are staged three ways because packaging differs:
# install() rules - Windows installer; on FHS/Flatpak they stage
# only uv (Flatpak's runtime comes from the deps
# build installing to /app/libpython; plain FHS
# installs bundle no python runtime)
# POST_BUILD copies (here) - build-tree runs, and the macOS .app/AppImage
# flows, which package the build tree and
# never run install()
# packaging scripts - build_release_macos.sh (copies the .app),
# build_linux_image.sh.in (assembles $APPDIR)
# Final locations relative to the executable:
# Windows: <exe dir>/python <resources>/tools/uv
# macOS: Contents/MacOS/python Contents/MacOS/tools/uv
# AppImage: $APPDIR/lib/python $APPDIR/resources/tools/uv
# Flatpak: /app/libpython (dep prefix) /app/share/OrcaSlicer/tools/uv
# Runtime lookup: PythonInterpreter::initialize() / bundled_uv_path().
# libpython resolution: rpath on the OrcaSlicer target (above); the dep
# recipe gives the bundled interpreter a self-relative rpath.
# --------------------------------------------------------------------------
if (WIN32)
# This has to be a separate target due to the windows command line length limits
add_custom_target(COPY_DLLS ALL DEPENDS OrcaSlicer)
@@ -228,6 +265,12 @@ if (WIN32)
VERBATIM
)
endif ()
# copy libpython to the bin folder for Windows
add_custom_command(TARGET OrcaSlicer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf "$<TARGET_FILE_DIR:OrcaSlicer>/python"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_PREFIX_PATH}/libpython" "$<TARGET_FILE_DIR:OrcaSlicer>/python"
COMMENT "Copying Python runtime into the build tree"
VERBATIM)
else ()
@@ -245,15 +288,19 @@ else ()
if (XCODE)
# Because of Debug/Release/etc. configurations (similar to MSVC) the slic3r binary is located in an extra level
set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/resources")
set(BIN_DIR "${CMAKE_CURRENT_BINARY_DIR}")
set(BIN_CONF_DIR "Debug")
else ()
set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/../resources")
set(BIN_DIR "$<TARGET_FILE_DIR:OrcaSlicer>")
endif ()
if (CMAKE_MACOSX_BUNDLE)
if (CMAKE_CONFIGURATION_TYPES)
set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/OrcaSlicer.app/Contents/Resources")
set(BIN_DIR "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/OrcaSlicer.app/Contents/MacOS")
else()
set(BIN_RESOURCES_DIR "${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.app/Contents/Resources")
set(BIN_DIR "${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.app/Contents/MacOS")
endif()
set(MACOSX_BUNDLE_ICON_FILE Icon.icns)
set(MACOSX_BUNDLE_BUNDLE_NAME "OrcaSlicer")
@@ -264,6 +311,21 @@ else ()
COMMAND ln -sfn "${SLIC3R_RESOURCES_DIR}" "${BIN_RESOURCES_DIR}"
COMMENT "Symlinking the resources directory into the build tree"
VERBATIM)
add_custom_command(TARGET OrcaSlicer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf "${BIN_DIR}/python"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_PREFIX_PATH}/libpython" "${BIN_DIR}/python"
COMMENT "Copying Python runtime into the build tree"
VERBATIM)
# Stage uv next to the binary for the build-tree/.app flows, which never
# run install() -- see the layout map above; lookup is bundled_uv_path().
if(ORCA_BUNDLED_UV_EXECUTABLE AND EXISTS "${ORCA_BUNDLED_UV_EXECUTABLE}")
add_custom_command(TARGET OrcaSlicer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${BIN_DIR}/tools/uv"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${ORCA_BUNDLED_UV_EXECUTABLE}" "${BIN_DIR}/tools/uv/${ORCA_BUNDLED_UV_FILENAME}"
COMMAND chmod +x "${BIN_DIR}/tools/uv/${ORCA_BUNDLED_UV_FILENAME}"
COMMENT "Copying bundled uv into the build tree"
VERBATIM)
endif()
endif ()
# Slic3r binary install target. Default build type is release in case no CMAKE_BUILD_TYPE is provided.
@@ -285,6 +347,7 @@ if (WIN32)
install(TARGETS OrcaSlicer_app_gui RUNTIME DESTINATION ".")
endif ()
install(FILES ${output_dlls_${build_type}} DESTINATION ".")
install(DIRECTORY "${CMAKE_PREFIX_PATH}/libpython/" DESTINATION "python")
else ()
install(TARGETS OrcaSlicer RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR})
endif ()
-1
View File
@@ -51,7 +51,6 @@ using namespace nlohmann;
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/GCode.hpp"
#include "libslic3r/GCode/PostProcessor.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "libslic3r/Platform.hpp"
+8
View File
@@ -263,6 +263,14 @@ int wmain(int argc, wchar_t **argv)
_wsplitpath(path_to_exe, drive, dir, fname, ext);
_wmakepath(path_to_exe, drive, dir, nullptr, nullptr);
wchar_t path_to_python[MAX_PATH + 1] = { 0 };
wcscpy(path_to_python, path_to_exe);
wcscat(path_to_python, L"python");
DWORD python_attrs = GetFileAttributesW(path_to_python);
if (python_attrs != INVALID_FILE_ATTRIBUTES && (python_attrs & FILE_ATTRIBUTE_DIRECTORY)) {
SetDllDirectoryW(path_to_python);
}
#ifdef SLIC3R_GUI
// https://wiki.qt.io/Cross_compiling_Mesa_for_Windows
// http://download.qt.io/development_releases/prebuilt/llvmpipe/windows/
@@ -171,6 +171,33 @@ echo -n "[9/9] Generating Linux app..."
fi
cp -fl "${ORIGINAL_BINARY_LOCATION}" "$BIN_DIR/@SLIC3R_APP_CMD@"
# Bundle the embedded Python runtime (interpreter + stdlib) that CMake staged
# next to the binary. OrcaSlicer resolves PYTHONHOME at <resources_dir>/../lib/python,
# which is $APPDIR/lib/python (resources_dir is $APPDIR/resources). Staging it here,
# before the dependency-closure pass below, lets that pass also bundle the extension
# modules' shared libraries; libpython itself is found via rpath ($ORIGIN entries set
# in src/CMakeLists.txt and by the dep recipe). Without this the plugin system cannot start.
PYTHON_RUNTIME_SRC="$(dirname "${ORIGINAL_BINARY_LOCATION}")/python"
if [ -d "$PYTHON_RUNTIME_SRC" ]; then
echo "Bundling Python runtime from ${PYTHON_RUNTIME_SRC} ..."
copy_directory_if_present "$PYTHON_RUNTIME_SRC" "$LIB_DIR/python"
else
echo "Warning: bundled Python runtime not found at ${PYTHON_RUNTIME_SRC}; plugin support will be disabled in this AppImage."
fi
# Bundle the uv executable (CMake staged it next to the binary) so the plugin
# system can install Python package dependencies. bundled_uv_path() looks under
# <resources_dir>/tools/uv == $APPDIR/resources/tools/uv -- the same layout the
# install()-based packagers (Windows/Flatpak/FHS) use. Unlike the AppImage, those
# run `make install`; this build copies artifacts, so stage uv explicitly.
UV_RUNTIME_SRC="$(dirname "${ORIGINAL_BINARY_LOCATION}")/tools/uv"
if [ -d "$UV_RUNTIME_SRC" ]; then
echo "Bundling uv from ${UV_RUNTIME_SRC} ..."
copy_directory_if_present "$UV_RUNTIME_SRC" "$APPDIR/resources/tools/uv"
else
echo "Warning: bundled uv not found at ${UV_RUNTIME_SRC}; Python package installation will be unavailable in this AppImage."
fi
if [ "$BUNDLE_DESKTOP_STACK" = "1" ]; then
GSTREAMER_PLUGIN_SOURCE_DIR="$(find_pkg_config_dir pluginsdir gstreamer-1.0 || true)"
if [ -z "$GSTREAMER_PLUGIN_SOURCE_DIR" ]; then
-2
View File
@@ -224,8 +224,6 @@ set(lisbslic3r_sources
GCode.hpp
GCode/PchipInterpolatorHelper.cpp
GCode/PchipInterpolatorHelper.hpp
GCode/PostProcessor.cpp
GCode/PostProcessor.hpp
GCode/PressureEqualizer.cpp
GCode/PressureEqualizer.hpp
GCode/PrintExtents.cpp
+80 -3
View File
@@ -4,6 +4,7 @@
#include "LocalesUtils.hpp"
#include "Preset.hpp"
#include <algorithm>
#include <assert.h>
#include <fstream>
#include <iostream>
@@ -36,6 +37,8 @@ using namespace nlohmann;
namespace Slic3r {
std::function<std::string(std::string, std::string)> ConfigBase::resolve_capability_fn = nullptr;
//BBS: add json support
//static const std::string CONFIG_VERSION_KEY = "version";
//static const std::string CONFIG_NAME_KEY = "name";
@@ -84,12 +87,12 @@ std::string escape_strings_cstyle(const std::vector<std::string> &strs)
// Separate the strings.
(*outptr ++) = ';';
const std::string &str = strs[j];
// Is the string simple or complex? Complex string contains spaces, tabs, new lines and other
// escapable characters. Empty string shall be quoted as well, if it is the only string in strs.
// Is the string simple or complex? Complex string contains spaces, tabs, semicolons, new lines
// and other escapable characters. Empty string shall be quoted as well, if it is the only string in strs.
bool should_quote = strs.size() == 1 && str.empty();
for (size_t i = 0; i < str.size(); ++ i) {
char c = str[i];
if (c == ' ' || c == '\t' || c == '\\' || c == '"' || c == '\r' || c == '\n') {
if (c == ' ' || c == '\t' || c == ';' || c == '\\' || c == '"' || c == '\r' || c == '\n') {
should_quote = true;
break;
}
@@ -1486,6 +1489,29 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
return std::move(substitutions_ctxt.substitutions);
}
std::optional<PluginCapabilityRef> parse_capability_ref(const std::string& value)
{
// Capability references are stored as "<plugin_name>;<cloud_uuid>;<capability_name>".
// The cloud UUID is empty for local plugins (two consecutive semicolons).
if (value.empty())
return std::nullopt;
const size_t first = value.find(';');
if (first == std::string::npos)
return std::nullopt;
const size_t second = value.find(';', first + 1);
if (second == std::string::npos)
return std::nullopt;
std::string name = value.substr(0, first);
std::string uuid = value.substr(first + 1, second - first - 1);
std::string capability_name = value.substr(second + 1);
if (name.empty() || capability_name.empty())
return std::nullopt;
return PluginCapabilityRef{ std::move(name), std::move(capability_name), std::move(uuid) };
}
//BBS: add json support
void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const
{
@@ -1495,6 +1521,8 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
j[BBL_JSON_KEY_NAME] = name;
j[BBL_JSON_KEY_FROM] = from;
std::vector<std::string> plugin_refs;
//record all the key-values
for (const std::string &opt_key : this->keys())
{
@@ -1520,6 +1548,28 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
json j_array(string_values);
j[opt_key] = j_array;
}
this->save_plugin_collection(opt_key, opt, plugin_refs);
}
// Lazily serialize the top-level "plugins" manifest: the individual plugin-backed options keep
// bare capability names, and the full "name;uuid;capability" references are derived here from
// those options via the registered resolver. Only do this when a resolver is available (GUI);
// without one (CLI/headless) leave whatever the "plugins" option already serialized above, so a
// round-trip never drops the manifest. De-duplicate while preserving order and skip empties.
if (resolve_capability_fn) {
std::vector<std::string> unique_refs;
unique_refs.reserve(plugin_refs.size());
for (std::string& ref : plugin_refs) {
if (ref.empty())
continue;
if (std::find(unique_refs.begin(), unique_refs.end(), ref) == unique_refs.end())
unique_refs.emplace_back(std::move(ref));
}
if (unique_refs.empty())
j.erase("plugins");
else
j["plugins"] = unique_refs;
}
boost::nowide::ofstream c;
@@ -1553,6 +1603,33 @@ void ConfigBase::null_nullables()
}
}
void ConfigBase::save_plugin_collection(const std::string& opt_key, const ConfigOption* opt, std::vector<std::string>& plugin_refs) const {
// Full plugin capability references ("name;uuid;capability") can only be derived through the
// resolver registered by the GUI once plugins are loaded. In non-GUI/headless contexts (e.g.
// the CLI) it stays null, so skip silently rather than calling an empty std::function.
if (!resolve_capability_fn)
return;
// Resolve a single bare capability value into its full reference and append it, skipping
// unset values and capabilities that could not be resolved (resolver returns "").
const auto append_ref = [&plugin_refs](const std::string& capability_value, const std::string& type) {
if (capability_value.empty())
return;
std::string ref = resolve_capability_fn(capability_value, type);
if (!ref.empty())
plugin_refs.emplace_back(std::move(ref));
};
if (opt_key == "post_process_plugin") {
const ConfigOptionVectorBase* vec = static_cast<const ConfigOptionVectorBase*>(opt);
for (const std::string& val : vec->vserialize())
append_ref(val, "post-processing");
} else if (opt_key == "printer_agent") {
append_ref((dynamic_cast<const ConfigOptionString *>(opt))->value, "printer-connection");
}
// Extend for other plugin-backed settings as needed.
}
DynamicConfig::DynamicConfig(const ConfigBase& rhs, const t_config_option_keys& keys)
{
for (const t_config_option_key& opt_key : keys)
+19 -1
View File
@@ -9,6 +9,7 @@
#include <cstdlib>
#include <functional>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
@@ -2226,6 +2227,7 @@ public:
legend,
// Vector value, but edited as a single string.
one_string,
plugin_picker,
};
// Identifier of this option. It is stored here so that it is accessible through the by_serialization_key_ordinal map.
@@ -2442,6 +2444,10 @@ public:
// "serialized" - vector valued option is entered in a single edit field. Values are separated by a semicolon.
// "show_value" - even if enum_values / enum_labels are set, still display the value, not the enum label.
std::string gui_flags;
// Optional plugin type used by GUIType::plugin_picker for filtering plugins.
std::string plugin_type;
// Indicate whether the option support plugin.
bool support_plugin { false };
// Label of the GUI input field.
// In case the GUI input fields are grouped in some views, the label defines a short label of a grouped value,
// while full_label contains a label of a stand-alone field.
@@ -2759,10 +2765,13 @@ public:
void null_nullables();
static size_t load_from_gcode_string_legacy(ConfigBase& config, const char* str, ConfigSubstitutionContext& substitutions);
static void set_resolve_capability_fn(std::function<std::string(std::string, std::string)> fn) { resolve_capability_fn = fn; }
private:
// Set a configuration value from a string.
bool set_deserialize_raw(const t_config_option_key& opt_key_src, const std::string& value, ConfigSubstitutionContext& substitutions, bool append);
void save_plugin_collection(const std::string& opt_key, const ConfigOption* opt, std::vector<std::string>& plugin_refs) const;
static std::function<std::string(std::string, std::string)> resolve_capability_fn;
};
// Configuration store with dynamic number of configuration values.
@@ -2999,6 +3008,15 @@ protected:
void set_defaults();
};
struct PluginCapabilityRef
{
std::string name;
std::string capability_name;
std::string uuid;
};
std::optional<PluginCapabilityRef> parse_capability_ref(const std::string& value);
}
#endif
-379
View File
@@ -1,379 +0,0 @@
#include "PostProcessor.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r/I18N.hpp"
#include <boost/algorithm/string.hpp>
#include <boost/log/trivial.hpp>
#include <boost/format.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/cstdlib.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/nowide/fstream.hpp>
// BBS
#include <iostream>
#include <fstream>
#ifdef WIN32
// The standard Windows includes.
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#include <shellapi.h>
// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
// This routine appends the given argument to a command line such that CommandLineToArgvW will return the argument string unchanged.
// Arguments in a command line should be separated by spaces; this function does not add these spaces.
// Argument - Supplies the argument to encode.
// CommandLine - Supplies the command line to which we append the encoded argument string.
static void quote_argv_winapi(const std::wstring &argument, std::wstring &commmand_line_out)
{
// Don't quote unless we actually need to do so --- hopefully avoid problems if programs won't parse quotes properly.
if (argument.empty() == false && argument.find_first_of(L" \t\n\v\"") == argument.npos)
commmand_line_out.append(argument);
else {
commmand_line_out.push_back(L'"');
for (auto it = argument.begin(); ; ++ it) {
unsigned number_backslashes = 0;
while (it != argument.end() && *it == L'\\') {
++ it;
++ number_backslashes;
}
if (it == argument.end()) {
// Escape all backslashes, but let the terminating double quotation mark we add below be interpreted as a metacharacter.
commmand_line_out.append(number_backslashes * 2, L'\\');
break;
} else if (*it == L'"') {
// Escape all backslashes and the following double quotation mark.
commmand_line_out.append(number_backslashes * 2 + 1, L'\\');
commmand_line_out.push_back(*it);
} else {
// Backslashes aren't special here.
commmand_line_out.append(number_backslashes, L'\\');
commmand_line_out.push_back(*it);
}
}
commmand_line_out.push_back(L'"');
}
}
static DWORD execute_process_winapi(const std::wstring &command_line)
{
// Extract the current environment to be passed to the child process.
std::wstring envstr;
{
wchar_t *env = GetEnvironmentStrings();
assert(env != nullptr);
const wchar_t* var = env;
size_t totallen = 0;
size_t len;
while ((len = wcslen(var)) > 0) {
totallen += len + 1;
var += len + 1;
}
envstr = std::wstring(env, totallen);
FreeEnvironmentStrings(env);
}
STARTUPINFOW startup_info;
memset(&startup_info, 0, sizeof(startup_info));
startup_info.cb = sizeof(STARTUPINFO);
#if 0
startup_info.dwFlags = STARTF_USESHOWWINDOW;
startup_info.wShowWindow = SW_HIDE;
#endif
PROCESS_INFORMATION process_info;
if (! ::CreateProcessW(
nullptr /* lpApplicationName */, (LPWSTR)command_line.c_str(), nullptr /* lpProcessAttributes */, nullptr /* lpThreadAttributes */, false /* bInheritHandles */,
CREATE_UNICODE_ENVIRONMENT /* | CREATE_NEW_CONSOLE */ /* dwCreationFlags */, (LPVOID)envstr.c_str(), nullptr /* lpCurrentDirectory */, &startup_info, &process_info))
throw Slic3r::RuntimeError(std::string("Failed starting the script ") + boost::nowide::narrow(command_line) + ", Win32 error: " + std::to_string(int(::GetLastError())));
::WaitForSingleObject(process_info.hProcess, INFINITE);
ULONG rc = 0;
::GetExitCodeProcess(process_info.hProcess, &rc);
::CloseHandle(process_info.hThread);
::CloseHandle(process_info.hProcess);
return rc;
}
// Run the script. If it is a perl script, run it through the bundled perl interpreter.
// If it is a batch file, run it through the cmd.exe.
// Otherwise run it directly.
static int run_script(const std::string &script, const std::string &gcode, std::string &/*std_err*/)
{
// Unpack the argument list provided by the user.
int nArgs;
LPWSTR *szArglist = CommandLineToArgvW(boost::nowide::widen(script).c_str(), &nArgs);
if (szArglist == nullptr || nArgs <= 0) {
// CommandLineToArgvW failed. Maybe the command line escapment is invalid?
throw Slic3r::RuntimeError(std::string("Post processing script ") + script + " on file " + gcode + " failed. CommandLineToArgvW() refused to parse the command line path.");
}
std::wstring command_line;
std::wstring command = szArglist[0];
if (! boost::filesystem::exists(boost::filesystem::path(command)))
throw Slic3r::RuntimeError(std::string("The configured post-processing script does not exist: ") + boost::nowide::narrow(command));
if (boost::iends_with(command, L".pl")) {
// This is a perl script. Run it through the perl interpreter.
// The current process may be slic3r.exe or slic3r-console.exe.
// Find the path of the process:
wchar_t wpath_exe[_MAX_PATH + 1];
::GetModuleFileNameW(nullptr, wpath_exe, _MAX_PATH);
boost::filesystem::path path_exe(wpath_exe);
boost::filesystem::path path_perl = path_exe.parent_path() / "perl" / "perl.exe";
if (! boost::filesystem::exists(path_perl)) {
LocalFree(szArglist);
throw Slic3r::RuntimeError(std::string("Perl interpreter ") + path_perl.string() + " does not exist.");
}
// Replace it with the current perl interpreter.
quote_argv_winapi(boost::nowide::widen(path_perl.string()), command_line);
command_line += L" ";
} else if (boost::iends_with(command, ".bat")) {
// Run a batch file through the command line interpreter.
command_line = L"cmd.exe /C ";
}
for (int i = 0; i < nArgs; ++ i) {
quote_argv_winapi(szArglist[i], command_line);
command_line += L" ";
}
LocalFree(szArglist);
quote_argv_winapi(boost::nowide::widen(gcode), command_line);
return (int)execute_process_winapi(command_line);
}
#else
// POSIX
#include <cstdlib> // getenv()
#include <sstream>
#include <boost/process.hpp>
namespace process = boost::process;
static int run_script(const std::string &script, const std::string &gcode, std::string &std_err)
{
// Try to obtain user's default shell
const char *shell = ::getenv("SHELL");
if (shell == nullptr) { shell = "/bin/sh"; }
// Quote and escape the gcode path argument
std::string command { script };
command.append(" '");
for (char c : gcode) {
if (c == '\'') { command.append("'\\''"); }
else { command.push_back(c); }
}
command.push_back('\'');
BOOST_LOG_TRIVIAL(debug) << boost::format("Executing script, shell: %1%, command: %2%") % shell % command;
process::ipstream istd_err;
process::child child(shell, "-c", command, process::std_err > istd_err);
std_err.clear();
std::string line;
while (child.running() && std::getline(istd_err, line)) {
std_err.append(line);
std_err.push_back('\n');
}
child.wait();
return child.exit_code();
}
#endif
namespace Slic3r {
//! macro used to mark string used at localization,
//! return same string
#define L(s) (s)
#define _(s) Slic3r::I18N::translate(s)
// BBS
void gcode_add_line_number(const std::string& path, const DynamicPrintConfig& config)
{
const ConfigOptionBool* opt = config.opt<ConfigOptionBool>("gcode_add_line_number");
if (!opt->getBool())
return;
auto gcode_file = boost::filesystem::path(path);
if (!boost::filesystem::exists(gcode_file))
return;
std::fstream fs;
std::string new_gcode;
fs.open(gcode_file.c_str(), std::fstream::in | std::fstream::out);
size_t line_number = 1;
std::string gcode_line;
while (std::getline(fs, gcode_line)) {
char num_str[128];
memset(num_str, 0, sizeof(num_str));
snprintf(num_str, sizeof(num_str), "%zd", line_number);
new_gcode += std::string("N") + num_str + " " + gcode_line + "\n";
line_number++;
}
fs.clear();
fs.seekp(0, std::ios_base::beg);
fs.write(new_gcode.c_str(), new_gcode.length());
fs.close();
}
// Run post processing script / scripts if defined.
// Returns true if a post-processing script was executed.
// Returns false if no post-processing script was defined.
// Throws an exception on error.
// host is one of "File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ...
// For a "File" target, a temp file will be created for src_path by adding a ".pp" suffix and src_path will be updated.
// In that case the caller is responsible to delete the temp file created.
// output_name is the final name of the G-code on SD card or when uploaded to PrusaLink or OctoPrint.
// If uploading to PrusaLink or OctoPrint, then the file will be renamed to output_name first on the target host.
// The post-processing script may change the output_name.
bool run_post_process_scripts(std::string &src_path, bool make_copy, const std::string &host, std::string &output_name, const DynamicPrintConfig &config)
{
const auto *post_process = config.opt<ConfigOptionStrings>("post_process");
if (// likely running in SLA mode
post_process == nullptr ||
// no post-processing script
post_process->values.empty())
return false;
std::string path;
if (make_copy) {
// Don't run the post-processing script on the input file, it will be memory mapped by the G-code viewer.
// Make a copy.
path = src_path + ".pp";
// First delete an old file if it exists.
try {
if (boost::filesystem::exists(path))
boost::filesystem::remove(path);
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed deleting an old temporary file %1% before running a post-processing script: %2%", path, err.what());
}
// Second make a copy.
std::string error_message;
if (copy_file(src_path, path, error_message, false) != SUCCESS)
throw Slic3r::RuntimeError(Slic3r::format("Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%", src_path, error_message));
} else {
// Don't make a copy of the G-code before running the post-processing script.
path = src_path;
}
auto delete_copy = [&path, &src_path, make_copy]() {
if (make_copy)
try {
if (boost::filesystem::exists(path))
boost::filesystem::remove(path);
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed deleting a temporary copy %1% of a G-code file %2% : %3%", path, src_path, err.what());
}
};
auto gcode_file = boost::filesystem::path(path);
if (! boost::filesystem::exists(gcode_file))
throw Slic3r::RuntimeError(std::string("Post-processor can't find exported gcode file"));
// Store print configuration into environment variables.
config.setenv_();
// Let the post-processing script know the target host ("File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ...)
boost::nowide::setenv("SLIC3R_PP_HOST", host.c_str(), 1);
// Let the post-processing script know the final file name. For "File" host, it is a full path of the target file name and its location, for example pointing to an SD card.
// For "PrusaLink" or "OctoPrint", it is a file name optionally with a directory on the target host.
boost::nowide::setenv("SLIC3R_PP_OUTPUT_NAME", output_name.c_str(), 1);
// Path to an optional file that the post-processing script may create and populate it with a single line containing the output_name replacement.
std::string path_output_name = path + ".output_name";
auto remove_output_name_file = [&path_output_name, &src_path]() {
try {
if (boost::filesystem::exists(path_output_name))
boost::filesystem::remove(path_output_name);
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed deleting a file %1% carrying the final name / path of a G-code file %2%: %3%", path_output_name, src_path, err.what());
}
};
// Remove possible stalled path_output_name of the previous run.
remove_output_name_file();
try {
for (const std::string &scripts : post_process->values) {
std::vector<std::string> lines;
boost::split(lines, scripts, boost::is_any_of("\r\n"));
for (std::string script : lines) {
// Ignore empty post processing script lines.
boost::trim(script);
if (script.empty())
continue;
BOOST_LOG_TRIVIAL(info) << "Executing script " << script << " on file " << path;
std::string std_err;
const int result = run_script(script, gcode_file.string(), std_err);
if (result != 0) {
const std::string msg = std_err.empty() ? (boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%") % script % path % result).str()
: (boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%\nOutput:\n%4%") % script % path % result % std_err).str();
BOOST_LOG_TRIVIAL(error) << msg;
delete_copy();
throw Slic3r::RuntimeError(msg);
}
if (! boost::filesystem::exists(gcode_file)) {
const std::string msg = (boost::format(_(L(
"Post-processing script %1% failed.\n\n"
"The post-processing script is expected to change the G-code file %2% in place, but the G-code file was deleted and likely saved under a new name.\n"
"Please adjust the post-processing script to change the G-code in place and consult the manual on how to optionally rename the post-processed G-code file.\n")))
% script % path).str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
}
}
if (boost::filesystem::exists(path_output_name)) {
try {
// Read a single line from path_output_name, which should contain the new output name of the post-processed G-code.
boost::nowide::fstream f;
f.open(path_output_name, std::ios::in);
std::string new_output_name;
std::getline(f, new_output_name);
f.close();
if (host == "File") {
namespace fs = boost::filesystem;
fs::path op(new_output_name);
if (op.is_relative() && op.has_filename() && op.parent_path().empty()) {
// Is this just a filename? Make it an absolute path.
auto outpath = fs::path(output_name).parent_path();
outpath /= op.string();
new_output_name = outpath.string();
}
else {
if (! op.is_absolute() || ! op.has_filename())
throw Slic3r::RuntimeError("Unable to parse desired new path from output name file");
}
if (! fs::exists(fs::path(new_output_name).parent_path()))
throw Slic3r::RuntimeError(Slic3r::format("Output directory does not exist: %1%",
fs::path(new_output_name).parent_path().string()));
}
BOOST_LOG_TRIVIAL(trace) << "Post-processing script changed the file name from " << output_name << " to " << new_output_name;
output_name = new_output_name;
} catch (const std::exception &err) {
throw Slic3r::RuntimeError(Slic3r::format("run_post_process_scripts: Failed reading a file %1% "
"carrying the final name / path of a G-code file: %2%",
path_output_name, err.what()));
}
remove_output_name_file();
}
} catch (...) {
remove_output_name_file();
delete_copy();
throw;
}
src_path = std::move(path);
return true;
}
} // namespace Slic3r
-34
View File
@@ -1,34 +0,0 @@
#ifndef slic3r_GCode_PostProcessor_hpp_
#define slic3r_GCode_PostProcessor_hpp_
#include <string>
#include "../libslic3r.h"
#include "../PrintConfig.hpp"
namespace Slic3r {
// Run post processing script / scripts if defined.
// Returns true if a post-processing script was executed.
// Returns false if no post-processing script was defined.
// Throws an exception on error.
// host is one of "File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ...
// If make_copy, then a temp file will be created for src_path by adding a ".pp" suffix and src_path will be updated.
// In that case the caller is responsible to delete the temp file created.
// output_name is the final name of the G-code on SD card or when uploaded to PrusaLink or OctoPrint.
// If uploading to PrusaLink or OctoPrint, then the file will be renamed to output_name first on the target host.
// The post-processing script may change the output_name.
extern bool run_post_process_scripts(std::string &src_path, bool make_copy, const std::string &host, std::string &output_name, const DynamicPrintConfig &config);
inline bool run_post_process_scripts(std::string &src_path, const DynamicPrintConfig &config)
{
std::string src_path_name = src_path;
return run_post_process_scripts(src_path, false, "File", src_path_name, config);
}
// BBS
extern void gcode_add_line_number(const std::string &path, const DynamicPrintConfig &config);
} // namespace Slic3r
#endif /* slic3r_GCode_PostProcessor_hpp_ */
+26
View File
@@ -105,6 +105,32 @@ PlatformFlavor platform_flavor()
return s_platform_flavor;
}
std::string platform_os_type()
{
#if defined(_WIN32)
return "win";
#elif defined(__APPLE__)
return "macos";
#elif defined(__linux__) || defined(__LINUX__)
return "linux";
#else
return "unknown";
#endif
}
std::string platform_architecture()
{
#if defined(__aarch64__) || defined(__arm64__) || defined(_M_ARM64)
return "arm64";
#elif defined(__x86_64__) || defined(__x86_64) || defined(__amd64__) || defined(__amd64) || defined(_M_X64) || defined(_M_AMD64)
return "x86_64";
#elif defined(__i386__) || defined(__i386) || defined(i386) || defined(_M_IX86)
return "i386";
#else
return "unknown";
#endif
}
std::string platform_to_string(Platform platform)
+2
View File
@@ -36,6 +36,8 @@ void detect_platform();
Platform platform();
PlatformFlavor platform_flavor();
std::string platform_os_type();
std::string platform_architecture();
std::string platform_to_string(Platform platform);
std::string platform_flavor_to_string(PlatformFlavor pf);
+14 -1
View File
@@ -1192,6 +1192,8 @@ static std::vector<std::string> s_Preset_print_options{
"min_feature_size",
"min_bead_width",
"post_process",
"post_process_plugin",
"plugins",
"process_change_extrusion_role_gcode",
"min_length_factor",
"wall_maximum_resolution",
@@ -3390,7 +3392,18 @@ void add_correct_opts_to_diff(const std::string &opt_key, t_config_option_keys&
for (int i = 0; i < int(opt_cur->values.size()); i++)
{
int init_id = i <= opt_init_max_id ? i : 0;
const bool is_new_index = i > opt_init_max_id;
int init_id = is_new_index ? 0 : i;
if (is_new_index) {
// Orca: intentional divergence from upstream. Any new vector index (at or
// beyond the reference vector's length) is flagged dirty unconditionally --
// independent of its value and nil-state -- so preset dirty-detection notices
// per-extruder/filament entries added by growth (e.g. extruder count). This
// applies to every vector option type routed through deep_diff().
// Covered by tests/libslic3r/test_preset_diff.cpp.
vec.emplace_back(opt_key + "#" + std::to_string(i));
continue;
}
if (opt_cur->values[i] != opt_init->values[init_id]) {
if (opt_cur->nullable()) {
if (opt_cur->is_nil(i)) {
+4 -4
View File
@@ -651,16 +651,16 @@ bool PresetBundle::use_bbl_network()
}
bool PresetBundle::use_bbl_device_tab() {
if (!is_bbl_vendor()) {
const auto cfg = printers.get_edited_preset().config;
if (!is_bbl_vendor())
return false;
}
if (use_bbl_network()) {
return true;
}
const auto cfg = printers.get_edited_preset().config;
// Use bbl device tab if printhost webui url is not set
// Use bbl device tab if printhost webui url is not set
return cfg.opt_string("print_host_webui").empty();
}
+3
View File
@@ -123,6 +123,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"printing_by_object_gcode",
"filament_end_gcode",
"post_process",
"post_process_plugin",
// "plugins" is the manifest backing post_process_plugin; like it, it only affects G-code export.
"plugins",
"extruder_clearance_height_to_rod",
"extruder_clearance_height_to_lid",
"extruder_clearance_radius",
+20 -1
View File
@@ -828,6 +828,7 @@ void PrintConfigDef::init_common_params()
def->tooltip = L("Select the network agent implementation for printer communication.");
def->mode = comAdvanced;
def->cli = ConfigOptionDef::nocli;
def->support_plugin = true;
def->set_default_value(new ConfigOptionString(""));
def = this->add("print_host", coString);
@@ -5102,7 +5103,25 @@ void PrintConfigDef::init_fff_params()
def->height = 5;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionString());
def = this->add("plugins", coStrings);
def->label = L("Plugins Used");
def->tooltip = L("Plugin capabilities referenced by this preset, stored as name;uuid;capability.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionStrings());
def = this->add("post_process_plugin", coStrings);
def->label = L("Post-processing Plugin");
def->tooltip = L("Select a Python plugin to process the output G-code. "
"Plugins are loaded from the orca_plugins directory in your data folder. "
"The plugin will receive the G-code file path and can modify it in place.");
def->gui_type = ConfigOptionDef::GUIType::plugin_picker;
def->plugin_type = "post-processing";
def->support_plugin = true;
def->full_width = true;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
def = this->add("printer_model", coString);
def->label = L("Printer type");
def->tooltip = L("Type of the printer.");
+1
View File
@@ -1570,6 +1570,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionBool, ooze_prevention))
((ConfigOptionString, filename_format))
((ConfigOptionStrings, post_process))
((ConfigOptionStrings, post_process_plugin))
((ConfigOptionString, printer_model))
((ConfigOptionFloat, resolution))
((ConfigOptionFloats, retraction_minimum_travel))
+83 -1
View File
@@ -38,6 +38,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Auxiliary.hpp
GUI/BackgroundSlicingProcess.cpp
GUI/BackgroundSlicingProcess.hpp
GUI/PostProcessor.cpp
GUI/PostProcessor.hpp
GUI/BBLStatusBarBind.cpp
GUI/BBLStatusBarBind.hpp
GUI/BBLStatusBar.cpp
@@ -107,6 +109,18 @@ set(SLIC3R_GUI_SOURCES
GUI/Downloader.hpp
GUI/DownloadProgressDialog.cpp
GUI/DownloadProgressDialog.hpp
GUI/PluginPickerDialog.cpp
GUI/PluginPickerDialog.hpp
GUI/PluginsDialog.cpp
GUI/PluginsDialog.hpp
GUI/ProcessRunner.cpp
GUI/ProcessRunner.hpp
GUI/TerminalDialog.cpp
GUI/TerminalDialog.hpp
GUI/PluginProgressDialog.cpp
GUI/PluginProgressDialog.hpp
GUI/PluginWebDialog.cpp
GUI/PluginWebDialog.hpp
GUI/DragCanvas.cpp
GUI/DragCanvas.hpp
GUI/EditGCodeDialog.cpp
@@ -564,11 +578,49 @@ set(SLIC3R_GUI_SOURCES
GUI/Widgets/TempInput.hpp
GUI/Widgets/TextInput.cpp
GUI/Widgets/TextInput.hpp
GUI/Widgets/WebViewHostDialog.cpp
GUI/Widgets/WebViewHostDialog.hpp
GUI/Widgets/WebView.cpp
GUI/Widgets/WebView.hpp
GUI/WipeTowerDialog.cpp
GUI/wxExtensions.cpp
GUI/wxExtensions.hpp
plugin/PythonInterpreter.cpp
plugin/PythonInterpreter.hpp
plugin/PythonFileUtils.cpp
plugin/PythonFileUtils.hpp
plugin/PythonPluginBridge.cpp
plugin/PythonPluginBridge.hpp
plugin/PythonPluginInterface.hpp
plugin/PyPluginPackage.hpp
plugin/PluginHostApi.cpp
plugin/PluginHostApi.hpp
plugin/PluginHostUi.cpp
plugin/PluginHostUi.hpp
plugin/CloudPluginService.cpp
plugin/CloudPluginService.hpp
plugin/PluginFsUtils.cpp
plugin/PluginFsUtils.hpp
plugin/PluginCatalog.cpp
plugin/PluginCatalog.hpp
plugin/PluginLoader.cpp
plugin/PluginLoader.hpp
plugin/PluginDescriptor.hpp
plugin/PluginManager.cpp
plugin/PluginManager.hpp
plugin/PluginAuditManager.cpp
plugin/PluginAuditManager.hpp
plugin/PluginResolver.cpp
plugin/PluginResolver.hpp
plugin/pluginTypes/gcode/GCodePluginCapability.hpp
plugin/pluginTypes/gcode/GCodePluginCapability.cpp
plugin/pluginTypes/gcode/GCodePluginCapabilityTrampoline.hpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp
plugin/pluginTypes/script/ScriptPluginCapability.hpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
pchheader.cpp
pchheader.hpp
Utils/ASCIIFolding.cpp
@@ -764,7 +816,7 @@ else()
set(_opengl_link_lib OpenGL::GL)
endif()
target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise)
target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo minilzo libvgcode md4c-html glad ${_opengl_link_lib} hidapi mdns ${wxWidgets_LIBRARIES} glfw libcurl OpenSSL::SSL OpenSSL::Crypto noise::noise pybind11::embed)
if (MSVC)
@@ -843,3 +895,33 @@ endif ()
# Add a definition so that we can tell we are compiling slic3r.
target_compile_definitions(libslic3r_gui PRIVATE SLIC3R_CURRENTLY_COMPILING_GUI_MODULE)
if(ORCA_BUNDLED_UV_EXECUTABLE_CONFIG)
target_compile_definitions(libslic3r_gui PRIVATE "ORCA_BUNDLED_UV_EXECUTABLE=\"${ORCA_BUNDLED_UV_EXECUTABLE_CONFIG}\"")
endif()
if (ORCA_BUILD_PYTHON_STUBGEN_MODULE)
add_library(orca_stubgen MODULE
plugin/PythonPluginBridge.cpp
)
target_compile_definitions(orca_stubgen PRIVATE
ORCA_PYTHON_STUBGEN_MODULE
)
target_include_directories(orca_stubgen PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
Utils
)
target_link_libraries(orca_stubgen PRIVATE
libslic3r_gui
pybind11::embed
)
set_target_properties(orca_stubgen PROPERTIES
OUTPUT_NAME orca
PREFIX ""
)
endif()
File diff suppressed because it is too large. Load diff
+150
View File
@@ -694,4 +694,154 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
}
}
static DevAms::AmsType ams_type_from_string(const std::string& s)
{
if (s == "ams_lite" || s == "ams-lite") return DevAms::AMS_LITE;
if (s == "n3f") return DevAms::N3F;
if (s == "n3s") return DevAms::N3S;
return DevAms::AMS; // default
}
void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system)
{
if (!system || !data.is_object())
return;
// --- AMS units ---
if (data.contains("units") && data["units"].is_array())
{
std::set<std::string> seen_units;
for (const auto& u : data["units"])
{
if (!u.is_object() || !u.contains("id"))
continue;
const std::string ams_id = u.value("id", std::string());
if (ams_id.empty())
continue;
seen_units.insert(ams_id);
const int ext_id = u.value("extruder", MAIN_EXTRUDER_ID);
const DevAms::AmsType type = ams_type_from_string(u.value("type", std::string("ams")));
DevAms* ams = nullptr;
auto it = system->amsList.find(ams_id);
if (it == system->amsList.end())
{
ams = new DevAms(ams_id, ext_id, type);
system->amsList.insert(std::make_pair(ams_id, ams));
}
else
{
ams = it->second;
ams->m_ext_id = ext_id;
ams->SetAmsType(type);
}
ams->m_exist = true;
ams->m_current_temperature = u.value("temperature", (float) INVALID_AMS_TEMPERATURE);
ams->m_humidity_percent = u.value("humidity_percent", -1);
ams->m_left_dry_time = u.value("dry_time_min", 0);
// --- slots / trays ---
std::set<std::string> seen_slots;
if (u.contains("slots") && u["slots"].is_array())
{
for (const auto& s : u["slots"])
{
if (!s.is_object())
continue;
const std::string tray_id = std::to_string(s.value("index", -1));
seen_slots.insert(tray_id);
DevAmsTray* tray = nullptr;
auto tit = ams->m_trays.find(tray_id);
if (tit == ams->m_trays.end())
{
tray = new DevAmsTray(tray_id);
ams->m_trays.insert(std::make_pair(tray_id, tray));
}
else
{
tray = tit->second;
}
tray->is_exists = s.value("loaded", false);
tray->m_fila_type = s.value("material", std::string());
tray->setting_id = s.value("preset_id", std::string());
tray->UpdateColorFromStr(s.value("color", std::string()));
tray->nozzle_temp_min = std::to_string(s.value("nozzle_temp_min", 0));
tray->nozzle_temp_max = std::to_string(s.value("nozzle_temp_max", 0));
tray->remain = s.value("remain_percent", -1);
tray->k = s.value("k", 0.0f);
if (s.contains("diameter_mm") && s["diameter_mm"].is_number())
tray->diameter = std::to_string(s["diameter_mm"].get<double>());
if (s.contains("weight_g") && s["weight_g"].is_number())
tray->weight = std::to_string(s["weight_g"].get<int>());
tray->cols.clear();
if (s.contains("colors") && s["colors"].is_array())
{
for (const auto& c : s["colors"])
if (c.is_string())
tray->cols.push_back(c.get<std::string>());
}
tray->ctype = tray->cols.size() > 1 ? 1 : 0;
}
}
// prune trays no longer reported
for (auto tit = ams->m_trays.begin(); tit != ams->m_trays.end();)
{
if (seen_slots.count(tit->first) == 0)
{
delete tit->second;
tit = ams->m_trays.erase(tit);
}
else
{
++tit;
}
}
}
// prune units no longer reported
for (auto it = system->amsList.begin(); it != system->amsList.end();)
{
if (seen_units.count(it->first) == 0)
{
delete it->second;
it = system->amsList.erase(it);
}
else
{
++it;
}
}
}
// --- external / direct spools -> obj->vt_slot ---
// extruder 0 -> main virtual slot, extruder >0 -> deputy.
if (obj && data.contains("external") && data["external"].is_array())
{
obj->vt_slot.clear();
for (const auto& e : data["external"])
{
if (!e.is_object())
continue;
const int ext = e.value("extruder", MAIN_EXTRUDER_ID);
const int vt_id = (ext == MAIN_EXTRUDER_ID) ? VIRTUAL_TRAY_MAIN_ID : VIRTUAL_TRAY_DEPUTY_ID;
DevAmsTray tray(std::to_string(vt_id));
tray.is_exists = e.value("loaded", false);
tray.m_fila_type = e.value("material", std::string());
tray.setting_id = e.value("preset_id", std::string());
tray.UpdateColorFromStr(e.value("color", std::string()));
tray.nozzle_temp_min = std::to_string(e.value("nozzle_temp_min", 0));
tray.nozzle_temp_max = std::to_string(e.value("nozzle_temp_max", 0));
tray.remain = e.value("remain_percent", -1);
obj->vt_slot.push_back(tray);
}
}
}
}
@@ -289,6 +289,8 @@ class DevFilaSystemParser
{
public:
static void ParseV1_0(const json& print_json, MachineObject* obj, DevFilaSystem* system, bool key_field_only);
static void ParseAgentFilament(const json& data, MachineObject* obj, DevFilaSystem* system);
};
}// namespace Slic3r
+457 -40
View File
@@ -10,7 +10,9 @@
#include "libslic3r/PrintConfig.hpp"
#include <algorithm>
#include <regex>
#include <utility>
#include <cstdint>
#include <wx/numformatter.h>
#include <wx/tooltip.h>
@@ -31,6 +33,7 @@
#include "Widgets/TextCtrl.h"
#include "../Utils/ColorSpaceConvert.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#ifdef __WXOSX__
#define wxOSX true
#else
@@ -484,17 +487,18 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true
}
const std::string sidetext = m_opt.sidetext.rfind("mm/s") != std::string::npos ? "mm/s" : "mm";
const wxString stVal = double_to_string(val, 2);
const wxString msg_text = from_u8((boost::format(_utf8(L("Is it %s%% or %s %s?\n"
"YES for %s%%, \n"
"NO for %s %s."))) % stVal % stVal % sidetext % stVal % stVal % sidetext).str());
const wxString stVal = double_to_string(val, 2);
const wxString msg_text = from_u8((boost::format(_utf8(L("Is it %s%% or %s %s?\n"
"YES for %s%%, \n"
"NO for %s %s."))) %
stVal % stVal % sidetext % stVal % stVal % sidetext)
.str());
WarningDialog dialog(m_parent, msg_text, _L("Parameter validation") + ": " + m_opt_id, wxYES | wxNO);
if ((val > 100) && dialog.ShowModal() == wxID_YES) {
set_value(from_u8((boost::format("%s%%") % stVal).str()), false/*true*/);
set_value(from_u8((boost::format("%s%%") % stVal).str()), false /*true*/);
str += "%%";
}
else
set_value(stVal, false); // it's no needed but can be helpful, when inputted value contained "," instead of "."
} else
set_value(stVal, false); // it's no needed but can be helpful, when inputted value contained "," instead of "."
}
}
if (m_opt.opt_key == "thumbnails") {
@@ -1325,6 +1329,39 @@ using choice_ctrl = ::ComboBox; // BBS
static std::map<std::string, DynamicList*> dynamic_lists;
static bool is_plugin_printer_agent_key(const std::string& value)
{
return value.rfind("plugin:", 0) == 0;
}
static int printer_agent_item_for_enum_index(const choice_ctrl* field, int enum_index)
{
if (!field)
return -1;
const unsigned int count = field->GetCount();
for (unsigned int idx = 0; idx < count; ++idx) {
if (void* data = field->GetClientData(idx)) {
const int stored = static_cast<int>(reinterpret_cast<uintptr_t>(data)) - 1;
if (stored == enum_index)
return static_cast<int>(idx);
}
}
return -1;
}
static int printer_agent_enum_index_for_item(const choice_ctrl* field, int item_index, int fallback)
{
if (!field || item_index < 0)
return fallback;
if (void* data = field->GetClientData(item_index))
return static_cast<int>(reinterpret_cast<uintptr_t>(data)) - 1;
return fallback;
}
void Choice::register_dynamic_list(std::string const &optname, DynamicList *list) { dynamic_lists.emplace(optname, list); }
void DynamicList::update()
@@ -1407,7 +1444,33 @@ void Choice::BUILD()
window = dynamic_cast<wxWindow*>(temp);
if (! m_opt.enum_labels.empty() || ! m_opt.enum_values.empty()) {
if (m_opt.enum_labels.empty()) {
if (m_opt_id == "printer_agent") {
const bool has_builtin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(),
[](const std::string& value) { return !is_plugin_printer_agent_key(value); });
const bool has_plugin_agents = std::any_of(m_opt.enum_values.begin(), m_opt.enum_values.end(),
[](const std::string& value) { return is_plugin_printer_agent_key(value); });
auto append_agent_rows = [this, temp](bool plugins) {
for (size_t i = 0; i < m_opt.enum_values.size(); ++i) {
const bool is_plugin = is_plugin_printer_agent_key(m_opt.enum_values[i]);
if (is_plugin != plugins)
continue;
const wxString label = i < m_opt.enum_labels.size() ? _(m_opt.enum_labels[i]) : wxString(m_opt.enum_values[i]);
const int item = temp->Append(label);
temp->SetClientData(item, reinterpret_cast<void*>(static_cast<uintptr_t>(i + 1)));
}
};
if (has_builtin_agents) {
temp->Append(_L("System agents"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
append_agent_rows(false);
}
if (has_plugin_agents) {
temp->Append(_L("Plugins"), wxNullBitmap, DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED);
append_agent_rows(true);
}
} else if (m_opt.enum_labels.empty()) {
// Append non-localized enum_values
for (auto el : m_opt.enum_values)
temp->Append(el);
@@ -1510,10 +1573,11 @@ void Choice::set_selection()
wxString text_value = wxString("");
choice_ctrl* field = dynamic_cast<choice_ctrl*>(window);
choice_ctrl* field = dynamic_cast<choice_ctrl*>(window);
switch (m_opt.type) {
case coEnum:{
field->SetSelection(m_opt.default_value->getInt());
const int val = m_opt.default_value->getInt();
field->SetSelection(m_opt_id == "printer_agent" ? printer_agent_item_for_enum_index(field, val) : val);
break;
}
case coFloat:
@@ -1562,9 +1626,15 @@ void Choice::set_value(const std::string& value, bool change_event) //! Redunda
++idx;
}
choice_ctrl* field = dynamic_cast<choice_ctrl*>(window);
idx == m_opt.enum_values.size() ?
field->SetValue(value) :
choice_ctrl* field = dynamic_cast<choice_ctrl*>(window);
if (m_opt_id == "printer_agent") {
const int enum_index = idx == m_opt.enum_values.size() ?
(m_opt.default_value ? m_opt.default_value->getInt() : 0) :
static_cast<int>(idx);
field->SetSelection(printer_agent_item_for_enum_index(field, enum_index));
} else if (idx == m_opt.enum_values.size())
field->SetValue(value);
else
field->SetSelection(idx);
m_disable_change_event = false;
@@ -1628,32 +1698,55 @@ void Choice::set_value(const boost::any& value, bool change_event)
case coEnum:
// BBS
case coEnums: {
int val = boost::any_cast<int>(value);
int selection = val;
auto printer_agent_index_from_key = [this](const std::string& key) {
auto it = std::find(m_opt.enum_values.begin(), m_opt.enum_values.end(), key);
if (it != m_opt.enum_values.end())
return static_cast<int>(it - m_opt.enum_values.begin());
return m_opt.default_value ? m_opt.default_value->getInt() : 0;
};
if (m_opt_id == "input_shaping_type") {
if (field != nullptr) {
const unsigned int count = field->GetCount();
int match_index = -1;
for (unsigned int idx = 0; idx < count; ++idx) {
if (void* data = field->GetClientData(idx)) {
int stored = static_cast<int>(reinterpret_cast<uintptr_t>(data));
if (stored == val) {
match_index = static_cast<int>(idx);
break;
}
}
}
if (match_index >= 0)
selection = match_index;
else if (val >= 0 && val < static_cast<int>(count))
selection = val;
else if (count > 0)
selection = 0;
else
selection = -1;
}
} else {
int val = 0;
if (m_opt_id == "printer_agent") {
if (const int* int_value = boost::any_cast<int>(&value))
val = *int_value;
else if (const wxString* wx_value = boost::any_cast<wxString>(&value))
val = printer_agent_index_from_key(into_u8(*wx_value));
else if (const std::string* string_value = boost::any_cast<std::string>(&value))
val = printer_agent_index_from_key(*string_value);
else {
m_disable_change_event = false;
return;
}
} else
val = boost::any_cast<int>(value);
int selection = val;
if (m_opt_id == "printer_agent") {
selection = printer_agent_item_for_enum_index(field, val);
} else if (m_opt_id == "input_shaping_type") {
if (field != nullptr) {
const unsigned int count = field->GetCount();
int match_index = -1;
for (unsigned int idx = 0; idx < count; ++idx) {
if (void* data = field->GetClientData(idx)) {
int stored = static_cast<int>(reinterpret_cast<uintptr_t>(data));
if (stored == val) {
match_index = static_cast<int>(idx);
break;
}
}
}
if (match_index >= 0)
selection = match_index;
else if (val >= 0 && val < static_cast<int>(count))
selection = val;
else if (count > 0)
selection = 0;
else
selection = -1;
}
} else {
// Support ThirdPartyPrinter
if (m_opt_id.compare("host_type") == 0 && val != 0 &&
m_opt.enum_values.size() > field->GetCount()) // for case, when PrusaLink isn't used as a HostType
@@ -1748,11 +1841,17 @@ boost::any& Choice::get_value()
if (m_opt_id == rp_option)
return m_value = boost::any(ret_str);
// BBS
// BBS
if (m_opt.type == coEnum || m_opt.type == coEnums)
{
if (m_opt.nullable && field->GetSelection() == -1)
m_value = ConfigOptionEnumsGenericNullable::nil_value();
else if (m_opt_id == "printer_agent")
{
const int selection = field->GetSelection();
const int fallback = m_opt.default_value ? m_opt.default_value->getInt() : 0;
m_value = printer_agent_enum_index_for_item(field, selection, fallback);
}
else if (m_opt_id == "input_shaping_type")
{
int selection = field->GetSelection();
@@ -1893,6 +1992,324 @@ void Choice::msw_rescale()
#endif
}
void PluginField::BUILD()
{
// Wrap the dynamic rows in a single container panel so the field is a proper window-field
// (getWindow() != null). The panel owns m_main_sizer; all row controls are children of it.
auto* panel = new wxPanel(m_parent, wxID_ANY);
panel->SetBackgroundStyle(wxBG_STYLE_PAINT);
wxGetApp().UpdateDarkUI(panel);
window = panel;
m_main_sizer = new wxBoxSizer(wxVERTICAL);
panel->SetSizer(m_main_sizer);
// Initialize with default values or empty
if (m_opt.type == coStrings) {
const ConfigOptionStrings* vec = m_opt.get_default_value<ConfigOptionStrings>();
if (vec != nullptr && !vec->values.empty()) {
m_values = vec->values;
}
}
rebuild_ui();
m_value = m_values;
}
void PluginField::set_selector(std::function<std::string()> selector)
{
m_selector = std::move(selector);
}
static void remove_empty_plugin_values(std::vector<std::string>& values)
{
for (auto it = values.begin(); it != values.end();) {
if (it->empty())
it = values.erase(it);
else
++it;
}
}
void PluginField::rebuild_ui()
{
remove_empty_plugin_values(m_values);
if (m_main_sizer) {
m_main_sizer->Clear(true);
}
m_rows.clear();
m_standalone_add_btn = nullptr;
if (m_values.empty()) {
add_empty_state_row();
} else {
for (size_t i = 0; i < m_values.size(); ++i)
add_plugin_row(display_name_for_value(m_values[i]), i == m_values.size() - 1);
}
if (window) {
// Report the stacked rows' size so the option-group / OG_CustomCtrl allocate the right height.
window->SetMinSize(m_main_sizer->CalcMin());
window->Layout();
}
if (m_parent)
m_parent->Layout();
}
void PluginField::add_empty_state_row()
{
const auto button_size = wxSize(def_width_thinner() * m_em_unit, -1);
auto row_sizer = new wxBoxSizer(wxHORIZONTAL);
wxTextCtrl* display = new wxTextCtrl(window, wxID_ANY, _L("No plugin selected"),
wxDefaultPosition, wxSize(def_width_wider() * m_em_unit, wxDefaultCoord),
wxTE_READONLY);
display->SetBackgroundStyle(wxBG_STYLE_PAINT);
display->SetEditable(false);
wxGetApp().UpdateDarkUI(display);
display->SetToolTip(_L("No plugin selected"));
auto add_btn = new ScalableButton(window, wxID_ANY, "param_add", wxEmptyString,
button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16);
wxGetApp().UpdateDarkUI(add_btn);
add_btn->SetToolTip(_L("Add plugin"));
add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_add_clicked(); });
row_sizer->Add(display, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
row_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL);
m_main_sizer->Add(row_sizer, 0, wxEXPAND | wxBOTTOM, 4);
PluginRow row;
row.display = display;
row.add_btn = add_btn;
row.sizer = row_sizer;
m_rows.push_back(row);
m_standalone_add_btn = add_btn;
}
void PluginField::add_plugin_row(const wxString& value, bool is_last)
{
const auto button_size = wxSize(def_width_thinner() * m_em_unit, -1);
auto row_sizer = new wxBoxSizer(wxHORIZONTAL);
// Select button with search icon
ScalableButton* select_btn = new ScalableButton(window, wxID_ANY, "search", wxEmptyString,
button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16);
wxGetApp().UpdateDarkUI(select_btn);
select_btn->SetToolTip(_L("Select plugin"));
// Display text control
wxTextCtrl* display = new wxTextCtrl(window, wxID_ANY, value,
wxDefaultPosition, wxSize(def_width_wider() * m_em_unit, wxDefaultCoord),
wxTE_READONLY);
display->SetBackgroundStyle(wxBG_STYLE_PAINT);
display->SetEditable(false);
wxGetApp().UpdateDarkUI(display);
display->SetToolTip(get_tooltip_text(value));
ScalableButton* remove_btn = nullptr;
if (!m_opt.readonly) {
remove_btn = new ScalableButton(window, wxID_ANY, "cross", wxEmptyString,
button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16);
wxGetApp().UpdateDarkUI(remove_btn);
remove_btn->SetToolTip(_L("Remove plugin"));
}
// Add button (only on last row)
ScalableButton* add_btn = nullptr;
if (is_last && !m_opt.readonly) {
add_btn = new ScalableButton(window, wxID_ANY, "param_add", wxEmptyString,
button_size, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true, 16);
wxGetApp().UpdateDarkUI(add_btn);
add_btn->SetToolTip(_L("Add plugin"));
add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_add_clicked(); });
}
const size_t row_index = m_rows.size();
select_btn->Bind(wxEVT_BUTTON, [this, row_index](wxCommandEvent&) { on_select_clicked(row_index); });
if (remove_btn)
remove_btn->Bind(wxEVT_BUTTON, [this, row_index](wxCommandEvent&) { on_remove_clicked(row_index); });
row_sizer->Add(select_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
row_sizer->Add(display, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
if (remove_btn)
row_sizer->Add(remove_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4);
if (add_btn)
row_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL);
else if (!m_opt.readonly) {
// Reserve space equal to the add button so all rows align.
row_sizer->Add(button_size.GetWidth(), button_size.GetHeight(), 0, wxALIGN_CENTER_VERTICAL);
}
m_main_sizer->Add(row_sizer, 0, wxEXPAND | wxBOTTOM, 4);
PluginRow row;
row.select_btn = select_btn;
row.display = display;
row.remove_btn = remove_btn;
row.add_btn = add_btn;
row.sizer = row_sizer;
m_rows.push_back(row);
}
wxString PluginField::display_name_for_value(const std::string& value) const
{
if (value.empty())
return _L("No plugin selected");
return from_u8(value);
}
void PluginField::on_select_clicked(size_t index)
{
if (index >= m_rows.size())
return;
if (!m_selector)
return;
std::string selected = m_selector();
if (selected.empty())
return;
if (index >= m_values.size())
m_values.resize(index + 1);
if (m_values[index] == selected)
return;
m_values[index] = selected;
set_row_value(index, display_name_for_value(selected));
m_value = m_values;
on_change_field();
}
void PluginField::on_add_clicked()
{
if (m_opt.readonly)
return;
if (!m_selector)
return;
std::string selected = m_selector();
if (selected.empty())
return;
m_values.push_back(selected);
m_value = m_values;
rebuild_ui();
on_change_field();
}
void PluginField::on_remove_clicked(size_t index)
{
if (m_opt.readonly || index >= m_values.size())
return;
m_values.erase(m_values.begin() + index);
m_value = m_values;
rebuild_ui();
on_change_field();
}
wxString PluginField::get_row_value(size_t index) const
{
if (index >= m_rows.size() || !m_rows[index].display)
return wxEmptyString;
return m_rows[index].display->GetValue();
}
void PluginField::set_row_value(size_t index, const wxString& value)
{
if (index >= m_rows.size() || !m_rows[index].display)
return;
m_rows[index].display->ChangeValue(value);
m_rows[index].display->SetToolTip(get_tooltip_text(value));
}
void PluginField::set_value(const boost::any& value, bool change_event)
{
m_disable_change_event = !change_event;
// Handle different input types
if (value.empty()) {
m_values.clear();
} else if (value.type() == typeid(std::vector<std::string>)) {
m_values = boost::any_cast<std::vector<std::string>>(value);
} else if (value.type() == typeid(wxString)) {
m_values.clear();
wxString text = boost::any_cast<wxString>(value);
if (!text.IsEmpty())
m_values.push_back(into_u8(text));
} else if (value.type() == typeid(std::string)) {
m_values.clear();
std::string text = boost::any_cast<std::string>(value);
if (!text.empty())
m_values.push_back(text);
}
rebuild_ui();
m_value = m_values;
m_disable_change_event = false;
if (change_event)
on_change_field();
}
boost::any& PluginField::get_value()
{
m_value = m_values;
return m_value;
}
void PluginField::enable()
{
for (auto& row : m_rows) {
if (row.select_btn)
row.select_btn->Enable();
if (row.display)
row.display->Enable();
if (row.remove_btn)
row.remove_btn->Enable();
if (row.add_btn)
row.add_btn->Enable();
}
if (m_standalone_add_btn)
m_standalone_add_btn->Enable();
}
void PluginField::disable()
{
for (auto& row : m_rows) {
if (row.select_btn)
row.select_btn->Disable();
if (row.display)
row.display->Disable();
if (row.remove_btn)
row.remove_btn->Disable();
if (row.add_btn)
row.add_btn->Disable();
}
if (m_standalone_add_btn)
m_standalone_add_btn->Disable();
}
void PluginField::msw_rescale()
{
Field::msw_rescale();
rebuild_ui();
}
void ColourPicker::BUILD()
{
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
+51
View File
@@ -466,6 +466,57 @@ public:
void suppress_scroll();
};
class PluginField : public Field {
using Field::Field;
public:
PluginField(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
PluginField(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
~PluginField() {}
void BUILD() override;
void set_selector(std::function<std::string()> selector);
void set_value(const boost::any& value, bool change_event = false) override;
boost::any& get_value() override;
void enable() override;
void disable() override;
// Window-field: the dynamic rows live inside a single container panel (assigned to the base
// `window` in BUILD), so the field exposes one window instead of a bare sizer. This lets focus/
// scroll, full-width sizing and teardown operate on the whole field.
wxWindow* getWindow() override { return window; }
void msw_rescale() override;
private:
struct PluginRow {
ScalableButton* select_btn { nullptr };
wxTextCtrl* display { nullptr };
ScalableButton* remove_btn { nullptr };
ScalableButton* add_btn { nullptr };
wxBoxSizer* sizer { nullptr };
};
void rebuild_ui();
void add_empty_state_row();
void add_plugin_row(const wxString& value = wxEmptyString, bool is_last = false);
wxString display_name_for_value(const std::string& value) const;
void on_select_clicked(size_t index);
void on_add_clicked();
void on_remove_clicked(size_t index);
wxString get_row_value(size_t index) const;
void set_row_value(size_t index, const wxString& value);
wxWindow* window { nullptr }; // container panel that hosts m_main_sizer
wxBoxSizer* m_main_sizer { nullptr };
std::vector<PluginRow> m_rows;
std::vector<std::string> m_values;
ScalableButton* m_standalone_add_btn { nullptr };
std::function<std::string()> m_selector;
};
class ColourPicker : public Field {
using Field::Field;
+5
View File
@@ -2999,6 +2999,11 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
_set_warning_notification(EWarning::MixtureFilamentIncompatible, !filament_mixture_compatible);
bool model_fits = contained_min_one && !m_model->objects.empty() && !partlyOut && object_results.filaments.empty() && tpu_valid && filament_printable;
// Honor the missing-plugin block so this geometry-only path does not re-enable slicing
// that Plater::validate_current_plate disabled (otherwise moving an object would briefly
// re-enable the Slice button while required plugins are still missing).
if (wxGetApp().plater() && wxGetApp().plater()->plugins_block_slicing())
model_fits = false;
post_event(Event<bool>(EVT_GLCANVAS_ENABLE_ACTION_BUTTONS, model_fits));
ppl.get_curr_plate()->update_slice_ready_status(model_fits);
}
+10
View File
@@ -174,6 +174,16 @@ void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt
config.option<ConfigOptionStrings>(opt_key)->values =
boost::any_cast<std::vector<std::string>>(value);
}
else if (config.def()->get(opt_key)->gui_type == ConfigOptionDef::GUIType::plugin_picker) {
if (value.type() == typeid(std::vector<std::string>)) {
config.option<ConfigOptionStrings>(opt_key)->values =
boost::any_cast<std::vector<std::string>>(value);
} else {
std::string str = boost::any_cast<std::string>(value);
config.option<ConfigOptionStrings>(opt_key)->values = str.empty() ?
std::vector<std::string>() : std::vector<std::string>{str};
}
}
else if (config.def()->get(opt_key)->gui_flags.compare("serialized") == 0) {
std::string str = boost::any_cast<std::string>(value);
std::vector<std::string> values {};
+247 -66
View File
@@ -1,6 +1,7 @@
#include "ExportPresetBundleDialog.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "libslic3r/Technologies.hpp"
#include "libslic3r/Platform.hpp"
#include "GUI_App.hpp"
#include "GUI_Init.hpp"
#include "GUI_ObjectList.hpp"
@@ -13,6 +14,7 @@
#include <boost/log/detail/native_typeof.hpp>
#include <libslic3r/Config.hpp>
#include <mutex>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/event.h>
// Localization headers: include libslic3r version first so everything in this file
@@ -76,6 +78,9 @@
#include "libslic3r/miniz_extension.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Color.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/PluginHostUi.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
#include "GUI.hpp"
#include "GUI_Utils.hpp"
@@ -135,6 +140,9 @@
#include "slic3r/Utils/BBLNetworkPlugin.hpp"
#include "slic3r/Utils/bambu_networking.hpp"
#include "PluginsDialog.hpp"
#include "TerminalDialog.hpp"
//#ifdef WIN32
//#include "BaseException.h"
//#endif
@@ -1113,6 +1121,12 @@ void GUI_App::shutdown()
if (m_is_recreating_gui) return;
stop_http_server();
set_closing(true);
Slic3r::PluginManager::instance().get_loader().set_shutting_down();
if (m_agent)
m_agent->set_printer_agent(nullptr);
NetworkAgentFactory::clear_printer_agent_cache();
BOOST_LOG_TRIVIAL(info) << "GUI_App::shutdown exit";
}
@@ -2217,6 +2231,14 @@ void GUI_App::init_networking_callbacks()
GUI_App::~GUI_App()
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": enter");
if (m_agent)
m_agent->set_printer_agent(nullptr);
NetworkAgentFactory::clear_printer_agent_cache();
Slic3r::PluginManager::instance().shutdown();
Slic3r::PythonInterpreter::instance().shutdown();
if (app_config != nullptr) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(": destroy app_config");
delete app_config;
@@ -3045,13 +3067,119 @@ bool GUI_App::on_init_inner()
wxMessageBox("Force using legacy bambu networking plugin because debugger is attached! If the app terminates itself immediately, please delete installed plugin and try again!");
}
} */
copy_network_if_available();
if (scrn) {
scrn->SetText(_L("Loading Plugins") + dots, 20);
wxYield();
}
on_init_network();
// Initialize plugins after network then register on_load callbacks so once the plugin loads finish, it gets registered automatically.
PluginManager& plugin_mgr = PluginManager::instance();
plugin_mgr.initialize();
ConfigBase::set_resolve_capability_fn([&plugin_mgr](const std::string& cap_name, const std::string& cap_type) {
auto plugin_cap = plugin_mgr.get_loader().try_get_plugin_capability_by_name_and_type(cap_name, plugin_capability_type_from_string(cap_type));
if (!plugin_cap)
return std::string();
PluginDescriptor descriptor;
if (!plugin_mgr.get_catalog().try_get_plugin_descriptor(plugin_cap->plugin_key, descriptor))
return std::string();
// Cloud plugins are resolved at runtime via the UUID in the middle field, so the first
// field keeps the friendly display name. Local plugins are looked up by plugin_key (the
// first field, with an empty UUID), so emit the plugin_key to keep them resolvable.
const std::string identity = descriptor.is_cloud_plugin() ? descriptor.name : descriptor.plugin_key;
return identity + ';' + descriptor.cloud_uuid() + ';' + cap_name;
});
// Set cloud plugin directory from previous session so cloud-installed
// plugins are discovered even before the network agent is ready.
const std::string preset_folder = app_config->get("preset_folder");
if (!preset_folder.empty()) {
plugin_mgr.get_catalog().set_cloud_plugin_dir(preset_folder);
plugin_mgr.get_loader().set_cloud_user_id(preset_folder);
}
plugin_mgr.discover_plugins(false, true);
auto refresh_plugins_dialog = [] {
if (!wxTheApp)
return;
GUI_App* app = &GUI::wxGetApp();
if (app->is_closing())
return;
app->CallAfter([app] {
if (!app->is_closing() && app->m_plugins_dlg)
app->m_plugins_dlg->update_plugin_dialog_ui();
});
};
plugin_mgr.get_loader().subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.get_loader().subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
plugin_mgr.get_loader().subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
plugin_mgr.get_loader().subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
plugin_mgr.get_loader().subscribe_on_capability_load_callback(
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name);
refresh_plugins_dialog();
// A newly loaded capability may satisfy a missing-plugin notification; re-validate the
// current plate (on the UI thread) so the notification clears once its plugin is available.
if (wxTheApp && !wxGetApp().is_closing())
wxGetApp().CallAfter([]() {
if (Plater* plater = wxGetApp().plater())
plater->revalidate_current_plate_if_plugins_missing();
});
});
plugin_mgr.get_loader().subscribe_on_capability_unload_callback(
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
if (capability.type == PluginCapabilityType::PrinterConnection)
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
refresh_plugins_dialog();
});
for (const std::string& plugin_key : plugin_mgr.get_catalog().get_enabled_plugin_keys()) {
if (!plugin_mgr.get_loader().is_plugin_loaded(plugin_key)) {
plugin_mgr.get_loader().load_plugin(plugin_mgr.get_catalog(), plugin_key, false);
BOOST_LOG_TRIVIAL(info) << "Auto-loading plugin on startup: " << plugin_key;
}
}
if (m_agent)
plugin_mgr.set_cloud_agent(std::dynamic_pointer_cast<OrcaCloudServiceAgent>(m_agent->get_cloud_agent()));
if (m_agent && m_agent->is_user_login()) {
enable_user_preset_folder(true);
plugin_mgr.get_catalog().set_cloud_plugin_dir(m_agent->get_user_id());
plugin_mgr.get_loader().set_cloud_user_id(m_agent->get_user_id());
// If there is a user logged in we do an immediate sync.
std::vector<std::string> not_found, unauthorized;
plugin_mgr.fetch_plugins_from_cloud(&not_found, &unauthorized);
if (plater()) {
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));
}
}
} else {
enable_user_preset_folder(false);
plugin_mgr.get_catalog().set_cloud_plugin_dir("");
plugin_mgr.get_loader().set_cloud_user_id("");
}
// BBS if load user preset failed
@@ -3543,52 +3671,54 @@ void GUI_App::switch_printer_agent()
// Read printer_agent from config, falling back to default
std::string effective_agent_id = ORCA_PRINTER_AGENT_ID;
std::string cloud_agent_id = ORCA_CLOUD_PROVIDER;
if (preset_bundle->is_bbl_vendor()) {
if (preset_bundle->is_bbl_vendor())
effective_agent_id = BBL_PRINTER_AGENT_ID;
cloud_agent_id = BBL_CLOUD_PROVIDER;
} else {
const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config;
if (config.has("printer_agent")) {
const std::string& value = config.option<ConfigOptionString>("printer_agent")->value;
if (!value.empty())
effective_agent_id = value;
}
const DynamicPrintConfig& config = preset_bundle->printers.get_edited_preset().config;
if (config.has("printer_agent")) {
const std::string& value = config.option<ConfigOptionString>("printer_agent")->value;
if (!value.empty())
effective_agent_id = value;
}
// Check if agent is registered
if (!NetworkAgentFactory::is_printer_agent_registered(effective_agent_id)) {
const PrinterAgentInfo* agent_info_ptr = NetworkAgentFactory::get_printer_agent_info(effective_agent_id);
if (!agent_info_ptr) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": unregistered agent ID '" << effective_agent_id
<< "', keeping current agent";
// Keep current agent, don't switch
return;
}
const PrinterAgentInfo agent_info = *agent_info_ptr;
std::string current_agent_id;
if (m_agent->get_printer_agent())
current_agent_id = m_agent->get_printer_agent()->get_agent_info().id;
std::string log_dir = data_dir();
std::string cloud_agent_id = agent_info.id == BBL_PRINTER_AGENT_ID ? BBL_CLOUD_PROVIDER : ORCA_CLOUD_PROVIDER;
std::shared_ptr<ICloudServiceAgent> cloud_agent = m_agent->get_cloud_agent(cloud_agent_id);
if (current_agent_id != effective_agent_id) {
std::string log_dir = data_dir();
std::shared_ptr<ICloudServiceAgent> cloud_agent = m_agent->get_cloud_agent(cloud_agent_id);
// Create new printer agent via registry
std::shared_ptr<IPrinterAgent> new_printer_agent =
NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, log_dir);
// Create new printer agent via registry
std::shared_ptr<IPrinterAgent> new_printer_agent =
NetworkAgentFactory::create_printer_agent_by_id(effective_agent_id, cloud_agent, log_dir);
if (!new_printer_agent) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id << "', keeping current agent";
return;
}
if (!new_printer_agent) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": failed to create agent '" << effective_agent_id << "', keeping current agent";
return;
}
if (m_agent->get_printer_agent() == new_printer_agent)
return;
// Swap the agent
m_agent->set_printer_agent(new_printer_agent);
sidebar().update_all_preset_comboboxes();
// Swap the agent
m_agent->set_printer_agent(new_printer_agent);
sidebar().update_all_preset_comboboxes();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": printer agent switched to " << effective_agent_id;
// Start discovery so Python agents can populate the device list via SSDP callback
m_agent->start_discovery(true, false);
{
// Auto-switch MachineObject
select_machine(effective_agent_id);
select_machine(agent_info.id);
}
}
@@ -4587,9 +4717,17 @@ void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PRO
remove_user_presets();
enable_user_preset_folder(false);
Slic3r::PluginManager::instance().get_loader().unload_cloud_plugins();
Slic3r::PluginManager::instance().get_catalog().clear_cloud_plugin_catalog();
Slic3r::PluginManager::instance().get_catalog().set_cloud_plugin_dir("");
Slic3r::PluginManager::instance().get_loader().set_cloud_user_id("");
preset_bundle->load_user_presets(DEFAULT_USER_FOLDER_NAME, ForwardCompatibilitySubstitutionRule::Enable);
mainframe->update_side_preset_ui();
// keep this here. refresh_from_catalog is meant to update the dialog UI.
if (m_plugins_dlg)
m_plugins_dlg->update_plugin_dialog_ui();
GUI::wxGetApp().stop_sync_user_preset();
}
@@ -5119,10 +5257,14 @@ void GUI_App::enable_user_preset_folder(bool enable)
std::string user_id = m_agent->get_user_id();
app_config->set("preset_folder", user_id);
GUI::wxGetApp().preset_bundle->update_user_presets_directory(user_id);
PluginManager::instance().get_catalog().set_cloud_plugin_dir(user_id);
PluginManager::instance().get_loader().set_cloud_user_id(user_id);
} else {
BOOST_LOG_TRIVIAL(info) << "preset_folder: set to empty";
app_config->set("preset_folder", "");
GUI::wxGetApp().preset_bundle->update_user_presets_directory(DEFAULT_USER_FOLDER_NAME);
PluginManager::instance().get_catalog().set_cloud_plugin_dir("");
PluginManager::instance().get_loader().set_cloud_user_id("");
}
}
@@ -5160,12 +5302,27 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
});
if (online_login && provider == ORCA_CLOUD_PROVIDER) {
// The steps below run synchronously on the UI thread (cloud plugin fetch and
// user-preset load both block on network/disk). Show an indeterminate progress
// dialog so the window isn't frozen without feedback. Percentages are cosmetic
// milestones, not measured progress.
ProgressDialog dlg(_L("Loading"), _L("Syncing your account…"), 100, mainframe, wxPD_AUTO_HIDE | wxPD_APP_MODAL);
dlg.Update(10, _L("Migrating presets…"));
maybe_migrate_user_presets_on_login();
remove_user_presets();
enable_user_preset_folder(true);
dlg.Update(40, _L("Fetching plugins…"));
PluginManager::instance().fetch_plugins_from_cloud();
if (m_plugins_dlg)
m_plugins_dlg->update_plugin_dialog_ui();
dlg.Update(70, _L("Loading user presets…"));
preset_bundle->load_user_presets(m_agent->get_user_id(provider), ForwardCompatibilitySubstitutionRule::Enable);
mainframe->update_side_preset_ui();
dlg.Update(100);
GUI::wxGetApp().mainframe->show_sync_dialog();
}
@@ -5179,7 +5336,7 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
void GUI_App::check_track_enable()
{
// Orca: telemetry only exists on the BBL cloud agent; always disable it.
// Orca: alaways disable track event
if (m_agent) {
m_agent->track_enable(false);
m_agent->track_remove_files();
@@ -5327,40 +5484,6 @@ struct UpdaterQuery
std::string os_info;
};
std::string detect_updater_os()
{
#if defined(_WIN32)
return "win";
#elif defined(__APPLE__)
return "macos";
#elif defined(__linux__) || defined(__LINUX__)
return "linux";
#else
return "unknown";
#endif
}
std::string detect_updater_arch()
{
#if defined(__aarch64__) || defined(_M_ARM64)
return "arm64";
#elif defined(__x86_64__) || defined(_M_X64)
return "x86_64";
#elif defined(__i386__) || defined(_M_IX86)
return "i386";
#else
std::string arch = wxPlatformInfo::Get().GetArchName().ToStdString();
boost::algorithm::to_lower(arch);
if (arch.find("aarch64") != std::string::npos || arch.find("arm64") != std::string::npos)
return "arm64";
if (arch.find("x86_64") != std::string::npos || arch.find("amd64") != std::string::npos)
return "x86_64";
if (arch.find("i686") != std::string::npos || arch.find("i386") != std::string::npos || arch.find("x86") != std::string::npos)
return "i386";
return "unknown";
#endif
}
std::string detect_updater_os_info()
{
wxString description = wxPlatformInfo::Get().GetOperatingSystemDescription();
@@ -5589,8 +5712,8 @@ void GUI_App::check_new_version_sf(bool show_tips, int by_user)
UpdaterQuery query{
detect_updater_iid(app_config),
detect_updater_version(),
detect_updater_os(),
detect_updater_arch(),
platform_os_type(),
platform_architecture(),
detect_updater_os_info()
};
@@ -8020,6 +8143,64 @@ void GUI_App::open_presetbundledialog(size_t open_on_tab, const std::string& hig
}
}
void GUI_App::open_plugins_dialog(size_t open_on_tab, const std::string& highlight_option)
{
if (m_plugins_dlg) {
m_plugins_dlg->Show();
m_plugins_dlg->Raise();
return;
}
try {
m_plugins_dlg = new PluginsDialog(mainframe, open_on_tab, highlight_option);
m_plugins_dlg->set_open_terminal_dlg_fn();
m_plugins_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& event) {
if (event.GetEventObject() == m_plugins_dlg)
m_plugins_dlg = nullptr;
event.Skip();
});
m_plugins_dlg->Show();
m_plugins_dlg->Raise();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "open_plugins_dialog failed: " << e.what();
if (m_plugins_dlg) {
m_plugins_dlg->Destroy();
m_plugins_dlg = nullptr;
}
wxMessageBox(wxString::Format(_L("Failed to open the Plugins dialog:\n%s"), from_u8(e.what())), _L("Plugins"),
wxOK | wxICON_ERROR, mainframe);
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "open_plugins_dialog failed with a non-standard exception";
if (m_plugins_dlg) {
m_plugins_dlg->Destroy();
m_plugins_dlg = nullptr;
}
wxMessageBox(_L("Failed to open the Plugins dialog (unknown error)."), _L("Plugins"), wxOK | wxICON_ERROR, mainframe);
}
}
void GUI_App::open_terminal_dialog()
{
if (m_terminal_dlg) {
m_terminal_dlg->Show();
m_terminal_dlg->Raise();
return;
}
m_terminal_dlg = new TerminalDialog(mainframe, wxID_ANY, _L("Plugin Terminal"),
wxDefaultPosition, wxSize(820, 600));
m_terminal_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& event) {
if (event.GetEventObject() == m_terminal_dlg)
m_terminal_dlg = nullptr;
event.Skip();
});
m_terminal_dlg->Show();
m_terminal_dlg->Raise();
}
void GUI_App::open_exportpresetbundledialog(size_t open_on_tab, const std::string& highlight_option)
{
bool app_layout_changed = false;
+7
View File
@@ -85,6 +85,8 @@ class HMSQuery;
class ModelMallDialog;
class PingCodeBindDialog;
class NetworkErrorDialog;
class PluginsDialog;
class TerminalDialog;
enum FileType
@@ -550,6 +552,9 @@ public:
void update_single_bundle(wxCommandEvent& evt);
PresetBundleDialog* m_preset_bundle_dlg{nullptr};
PluginsDialog* m_plugins_dlg{nullptr};
TerminalDialog* m_terminal_dlg{nullptr};
void start_http_server(const std::string& provider = ORCA_CLOUD_PROVIDER);
void start_http_server(int port, const std::string& provider = ORCA_CLOUD_PROVIDER);
@@ -617,6 +622,8 @@ public:
void open_preferences(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
void open_presetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
void open_plugins_dialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
void open_terminal_dialog();
void open_exportpresetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
virtual bool OnExceptionInMainLoop() override;
// Calls wxLaunchDefaultBrowser if user confirms in dialog.
+41 -20
View File
@@ -1,5 +1,9 @@
#include "MainFrame.hpp"
#include <wx/event.h>
#ifdef __WXGTK__
#include <wx/gtk/app.h>
#endif
#include <wx/panel.h>
#include <wx/notebook.h>
#include <wx/listbook.h>
@@ -46,6 +50,7 @@
#include "Widgets/ProgressDialog.hpp"
#include "BindDialog.hpp"
#include "../Utils/MacDarkMode.hpp"
#include "../Utils/NetworkAgentFactory.hpp"
#include "../Utils/PrintHost.hpp"
#include <fstream>
@@ -2809,21 +2814,6 @@ void MainFrame::init_menubar_as_editor()
append_submenu(fileMenu, export_menu, wxID_ANY, _L("Export"), "");
fileMenu->AppendSeparator();
append_menu_item(fileMenu, wxID_ANY, _L("Sync Presets"), _L("Pull and apply the latest presets from OrcaCloud"),
[this](wxCommandEvent&) {
if (!wxGetApp().is_user_login()) {
MessageDialog info_dlg(this, _L("You must be logged in to sync presets from cloud."),
_L("Sync Presets"), wxOK | wxICON_INFORMATION);
info_dlg.ShowModal();
return;
}
wxGetApp().restart_sync_user_preset();
}, "", nullptr,
[this]() {
return wxGetApp().is_user_login() && !wxGetApp().app_config->get_stealth_mode();
}, this);
fileMenu->AppendSeparator();
#ifndef __APPLE__
append_menu_item(fileMenu, wxID_EXIT, _L("Quit"), wxString::Format(_L("Quit")),
@@ -3289,8 +3279,10 @@ void MainFrame::init_menubar_as_editor()
},
"", nullptr, []() { return true; }, this);
auto top_menu = m_topbar->GetTopMenu();
top_menu->AppendSeparator();
append_menu_item(
m_topbar->GetTopMenu(), wxID_ANY, _L("Preset Bundle") + "\t", "",
top_menu, wxID_ANY, _L("Preset Bundle") + "\t", "",
[this](wxCommandEvent &) {
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
wxGetApp().open_presetbundledialog();
@@ -3299,7 +3291,7 @@ void MainFrame::init_menubar_as_editor()
"", nullptr, []() { return true; }, this);
append_menu_item(
m_topbar->GetTopMenu(), wxID_ANY, _L("Sync Presets"), _L("Pull and apply the latest presets from OrcaCloud"),
top_menu, wxID_ANY, _L("Sync Presets"), _L("Pull and apply the latest presets from OrcaCloud"),
[this](wxCommandEvent&) {
if (!wxGetApp().is_user_login()) {
MessageDialog info_dlg(this, _L("You must be logged in to sync presets from cloud."),
@@ -3316,10 +3308,20 @@ void MainFrame::init_menubar_as_editor()
return wxGetApp().is_user_login() && !wxGetApp().app_config->get_stealth_mode();
}, this);
top_menu->AppendSeparator();
append_menu_item(
top_menu, wxID_ANY, _L("Plugins") + "\t", "",
[this](wxCommandEvent &) {
wxGetApp().open_plugins_dialog();
},
"", nullptr, []() { return true; }, this);
//m_topbar->AddDropDownMenuItem(preference_item);
//m_topbar->AddDropDownMenuItem(printer_item);
//m_topbar->AddDropDownMenuItem(language_item);
//m_topbar->AddDropDownMenuItem(config_item);
top_menu->AppendSeparator();
m_topbar->AddDropDownSubMenu(helpMenu, _L("Help"));
// SoftFever calibrations
@@ -3420,11 +3422,31 @@ void MainFrame::init_menubar_as_editor()
fileMenu->AppendSeparator();
append_menu_item(
fileMenu, wxID_ANY, _L("Preset Bundle"), "",
[this](wxCommandEvent &) {
[this](wxCommandEvent&) {
wxGetApp().open_presetbundledialog();
plater()->get_current_canvas3D()->force_set_focus();
},
"", nullptr, []() { return true; }, this);
append_menu_item(
fileMenu, wxID_ANY, _L("Sync Presets"), _L("Pull and apply the latest presets from OrcaCloud"),
[this](wxCommandEvent&) {
if (!wxGetApp().is_user_login()) {
MessageDialog info_dlg(this, _L("You must be logged in to sync presets from cloud."), _L("Sync Presets"),
wxOK | wxICON_INFORMATION);
info_dlg.ShowModal();
return;
}
wxGetApp().restart_sync_user_preset();
},
"", nullptr, [this]() { return wxGetApp().is_user_login() && !wxGetApp().app_config->get_stealth_mode(); }, this);
fileMenu->AppendSeparator();
append_menu_item(
fileMenu, wxID_ANY, _L("Plugins"), "", [this](wxCommandEvent&) { wxGetApp().open_plugins_dialog(); }, "", nullptr,
[]() { return true; }, this);
fileMenu->AppendSeparator();
m_menubar->Append(fileMenu, wxString::Format("&%s", _L("File")));
if (editMenu)
m_menubar->Append(editMenu, wxString::Format("&%s", _L("Edit")));
@@ -4229,7 +4251,7 @@ void MainFrame::load_printer_url(wxString url, wxString apikey)
void MainFrame::load_printer_url()
{
PresetBundle &preset_bundle = *wxGetApp().preset_bundle;
if (preset_bundle.use_bbl_device_tab())
if (preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin())
return;
auto cfg = preset_bundle.printers.get_edited_preset().config;
@@ -4437,4 +4459,3 @@ void SettingsDialog::on_dpi_changed(const wxRect& suggested_rect)
} // GUI
} // Slic3r
+87
View File
@@ -2447,6 +2447,93 @@ void NotificationManager::push_orca_sync_conflict_notification(const std::string
data, m_id_provider, m_evt_handler, std::move(pull_callback), std::move(force_push_callback), conflict_code), 0);
}
void NotificationManager::PluginMissingNotification::init()
{
PopNotification::init();
// Reserve body rows, an optional spacer, and a dedicated action row for the two links.
m_lines_count = m_lines_count + m_body.size() + (m_body.empty() ? 0 : 1) + 1;
}
void NotificationManager::PluginMissingNotification::render_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y)
{
float x_offset = m_left_indentation;
float shift_y = m_line_height;
float starting_y = m_line_height / 2;
int last_end = 0;
std::string line;
for (size_t i = 0; i < m_endlines.size(); i++) {
if (m_text1.size() >= m_endlines[i]) {
line = m_text1.substr(last_end, m_endlines[i] - last_end);
last_end = m_endlines[i];
if (m_text1.size() > m_endlines[i])
last_end += (m_text1[m_endlines[i]] == '\n' || m_text1[m_endlines[i]] == ' ' ? 1 : 0);
ImGui::SetCursorPosX(x_offset);
ImGui::SetCursorPosY(starting_y + i * shift_y);
imgui.text(line.c_str());
}
}
const size_t body_start_row = m_endlines.size();
const std::string jump_text = _u8L("Jump to");
for (size_t i = 0; i < m_body.size(); ++i) {
const JumpTo& item = m_body[i];
const std::string item_text = item.text.empty() ? item.opt : item.text;
const std::string prefix = "- " + item_text + " ";
const float row_y = starting_y + (body_start_row + i) * shift_y;
ImGui::SetCursorPosX(x_offset);
ImGui::SetCursorPosY(row_y);
imgui.text(prefix.c_str());
std::string button_id = "##plugin_missing_jump_" + std::to_string(i);
const float jump_x = x_offset + ImGui::CalcTextSize(prefix.c_str()).x;
render_hyperlink_action(imgui, jump_x, row_y, jump_text, button_id.c_str(), [item] {
// Defer the jump: jump_to_option switches the settings tab/page, which must not run
// inside this notification's ImGui render pass. item is captured by value.
if (!item.opt.empty())
wxGetApp().CallAfter([item]() { wxGetApp().sidebar().jump_to_option(item.opt, item.opt_type, L""); });
});
}
const size_t action_row = body_start_row + m_body.size() + (m_body.empty() ? 0 : 1);
const float action_y = starting_y + action_row * shift_y;
render_hyperlink_action(imgui, x_offset, action_y, m_resolve_label, "##plugin_missing_resolve",
[this] { if (m_resolve_callback && m_resolve_callback(m_evt_handler)) close(); });
}
void NotificationManager::PluginMissingNotification::bbl_render_block_notif_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y)
{
const ImVec4 hyper_text_color = m_HyperTextColor;
const ImVec4 hyper_text_color_hover = m_HyperTextColorHover;
m_HyperTextColor = ImVec4(1.f, 1.f, 1.f, 1.f);
m_HyperTextColorHover = ImVec4(1.f, 1.f, 1.f, 0.75f);
render_text(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y);
m_HyperTextColor = hyper_text_color;
m_HyperTextColorHover = hyper_text_color_hover;
}
void NotificationManager::push_plugin_missing_notification(NotificationType type,
const std::string& text,
const std::string& resolve_label,
std::vector<JumpTo> body,
std::function<bool(wxEvtHandler*)> resolve_callback)
{
m_pop_notifications.erase(std::remove_if(m_pop_notifications.begin(), m_pop_notifications.end(),
[type](const std::unique_ptr<PopNotification>& notification) {
return notification && notification->get_type() == type;
}), m_pop_notifications.end());
NotificationData data{ type, NotificationLevel::ErrorNotificationLevel, 0, text };
push_notification_data(std::make_unique<NotificationManager::PluginMissingNotification>(
data, m_id_provider, m_evt_handler, resolve_label, std::move(body), std::move(resolve_callback)), 0);
}
void NotificationManager::push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback)
{
// If already exists
+66
View File
@@ -21,6 +21,8 @@
#include <deque>
#include <unordered_set>
#include <libslic3r/Preset.hpp>
namespace Slic3r {
namespace GUI {
@@ -44,6 +46,13 @@ enum class InfoItemType;
#define BBL_NOTICE_MAX_INTERVAL 86400 * 10
struct JumpTo
{
std::string text;
std::string opt;
Preset::Type opt_type;
};
enum class NotificationType
{
CustomNotification = 0,
@@ -166,6 +175,16 @@ enum class NotificationType
OrcaSharedProfilesAvailable,
OrcaCloudAPIError,
OrcaSyncConflict,
// Active preset requires plugin capabilities that are not installed/loadable. Persistent,
// non-modal; offers install (cloud) / OrcaCloud search (local) and blocks slicing.
OrcaLocalPluginMissingError,
OrcaCloudPluginMissingError,
// Active preset references capabilities that are installed but not active (plugin not loaded, or
// capability disabled). Resolved locally by activating them; persistent, blocks slicing.
OrcaPluginInactiveError,
// Active preset references a capability the installed+loaded plugin does not provide (outdated
// plugin). Informational; cannot be auto-resolved; persistent, blocks slicing.
OrcaPluginCapabilityUnavailableError,
NotificationTypeCount
};
@@ -282,6 +301,13 @@ public:
int conflict_code,
std::function<bool(wxEvtHandler*)> pull_callback,
std::function<bool(wxEvtHandler*)> force_push_callback);
// Non-closable, persistent missing-plugin notification with a single resolve action (install /
// open OrcaCloud). The callback returns true to close the notification, or false to keep it
// visible while resolution continues.
void push_plugin_missing_notification(NotificationType type, const std::string& text,
const std::string& resolve_label,
std::vector<JumpTo> body,
std::function<bool(wxEvtHandler*)> resolve_callback);
// Download URL progress notif
void push_download_URL_progress_notification(size_t id, const std::string& text, std::function<bool(DownloaderUserAction, int)> user_action_callback);
@@ -925,6 +951,46 @@ private:
std::function<bool(wxEvtHandler*)> m_force_push_callback;
int conflict_code;
};
// Persistent, non-closable notification for preset plugin capabilities that are required but
// unavailable. Offers per-capability "Jump to" links and a single resolve action; it stays up
// until every missing plugin is resolved.
class PluginMissingNotification : public PopNotification
{
public:
PluginMissingNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler,
std::string resolve_label,
std::vector<JumpTo> body,
std::function<bool(wxEvtHandler*)> resolve_callback)
: PopNotification(n, id_provider, evt_handler)
, m_resolve_label(std::move(resolve_label))
, m_body(std::move(body))
, m_resolve_callback(std::move(resolve_callback))
{
m_multiline = true;
}
protected:
void init() override;
void render_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y) override;
// Non-closable: the notification stays up until the missing plugins are resolved.
void render_close_button(ImGuiWrapper& /*imgui*/,
const float /*win_size_x*/, const float /*win_size_y*/,
const float /*win_pos_x*/, const float /*win_pos_y*/) override {}
void render_minimize_button(ImGuiWrapper& /*imgui*/,
const float /*win_pos_x*/, const float /*win_pos_y*/) override { m_minimize_b_visible = false; }
void bbl_render_block_notif_text(ImGuiWrapper& imgui,
const float win_size_x, const float win_size_y,
const float win_pos_x, const float win_pos_y) override;
void bbl_render_block_notif_buttons(ImGuiWrapper& /*imgui*/,
ImVec2 /*win_size*/, ImVec2 /*win_pos*/) override {}
std::string m_resolve_label;
std::vector<JumpTo> m_body;
std::function<bool(wxEvtHandler*)> m_resolve_callback;
};
class SlicingProgressNotification;
// in HintNotification.hpp
File diff suppressed because it is too large. Load diff
+5 -1
View File
@@ -106,6 +106,7 @@ public:
wxWindow * stb;
const wxString icon;
const wxString title;
bool m_labels_hidden{false};
size_t label_width = 20 ;// {200};
wxSizer* sizer {nullptr};
OG_CustomCtrl* custom_ctrl{ nullptr };
@@ -185,7 +186,7 @@ public:
void clear_fields_except_of(const std::vector<std::string> left_fields);
void hide_labels() { label_width = 0; }
void hide_labels() { label_width = 0; m_labels_hidden = true; }
OptionsGroup(wxWindow *_parent, const wxString &title, const wxString &icon, bool is_tab_opt = false,
column_t extra_clmn = nullptr);
@@ -243,6 +244,9 @@ protected:
public:
static wxString get_url(const std::string& path_end);
static bool launch_browser(const std::string& path_end);
protected:
std::string pick_plugin(const ConfigOptionDef& opt);
};
class ConfigOptionsGroup: public OptionsGroup {
+245 -7
View File
@@ -1,15 +1,25 @@
#include "Plater.hpp"
#include "../Utils/NetworkAgent.hpp"
#include "../Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r_version.h"
#include <cstddef>
#include <algorithm>
#include <chrono>
#include <numeric>
#include <limits>
#include <optional>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PluginResolver.hpp>
#include <vector>
#include <string>
#include <regex>
#include <future>
#include <thread>
#include <atomic>
#include <mutex>
#include <boost/algorithm/string.hpp>
#include <boost/iterator/counting_iterator.hpp>
#include <boost/optional.hpp>
@@ -21,6 +31,7 @@
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <wx/msgdlg.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/button.h>
@@ -37,6 +48,8 @@
#include <wx/debug.h>
#include <wx/busyinfo.h>
#include <wx/event.h>
#include <wx/evtloop.h>
#include <wx/timer.h>
#include <wx/wrapsizer.h>
#ifdef _WIN32
#include <wx/richtooltip.h>
@@ -2502,6 +2515,7 @@ void Sidebar::update_all_preset_comboboxes()
auto p_mainframe = wxGetApp().mainframe;
auto cfg = preset_bundle.printers.get_edited_preset().config;
const bool use_native_device_tab = preset_bundle.use_bbl_device_tab() || NetworkAgentFactory::is_current_printer_agent_plugin();
if (preset_bundle.use_bbl_network()) {
//only show connection button for not-BBL printer
@@ -2538,7 +2552,8 @@ void Sidebar::update_all_preset_comboboxes()
print_btn_type = preset_bundle.is_bbl_vendor() ? MainFrame::PrintSelectType::ePrintPlate : MainFrame::PrintSelectType::eSendGcode;
}
p_mainframe->load_printer_url(url, apikey);
if (!use_native_device_tab)
p_mainframe->load_printer_url(url, apikey);
p_mainframe->set_print_button_to_default(print_btn_type);
@@ -2608,8 +2623,7 @@ void Sidebar::update_all_preset_comboboxes()
update_printer_thumbnail();
}
// Orca:: show device tab based on vendor type
p_mainframe->show_device(preset_bundle.use_bbl_device_tab());
p_mainframe->show_device(use_native_device_tab);
p_mainframe->m_tabpanel->SetSelection(p_mainframe->m_tabpanel->GetSelection());
}
@@ -4458,6 +4472,14 @@ struct Plater::priv
bool m_ignore_event{false};
bool m_slice_all{false};
bool m_is_slicing {false};
// Missing-plugin set signatures (sorted full refs joined by '\n'), one per notification. They
// gate plugin-load re-validation and avoid needlessly recreating the notification when the set
// is unchanged. Whether missing plugins block slicing is derived directly from PluginResolver
// (has_missing_plugins()), not cached here.
std::string m_local_missing_shown_sig;
std::string m_cloud_missing_shown_sig;
std::string m_inactive_shown_sig;
std::string m_broken_shown_sig;
bool auto_reslice_pending {false};
bool auto_reslice_after_cancel {false};
bool m_is_publishing {false};
@@ -10361,8 +10383,13 @@ void Plater::priv::on_tab_selection_changing(wxBookCtrlEvent& e)
sidebar_layout.show = new_sel == MainFrame::tp3DEditor || new_sel == MainFrame::tpPreview;
update_sidebar();
int old_sel = e.GetOldSelection();
if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->use_bbl_device_tab() && new_sel == MainFrame::tpMonitor) {
if (!Slic3r::NetworkAgent::is_network_module_loaded()) {
const bool is_printer_agent_plugin = NetworkAgentFactory::is_current_printer_agent_plugin();
const bool use_native_device_tab = wxGetApp().preset_bundle &&
(wxGetApp().preset_bundle->use_bbl_device_tab() || is_printer_agent_plugin);
if (use_native_device_tab && new_sel == MainFrame::tpMonitor) {
// BBL network module is only required for BBL-vendor printers.
// Non-BBL Python plugins (e.g. moonraker) drive the Device tab without it.
if (!is_printer_agent_plugin && wxGetApp().preset_bundle->is_bbl_vendor() && !Slic3r::NetworkAgent::is_network_module_loaded()) {
e.Veto();
BOOST_LOG_TRIVIAL(info) << boost::format("skipped tab switch from %1% to %2%, lack of network plugins") % old_sel % new_sel;
if (q) {
@@ -15944,7 +15971,18 @@ void Plater::reslice()
// and notify user that he should leave it first.
if (get_view3D_canvas3D()->get_gizmos_manager().is_in_editing_mode(true))
return;
// Keep slicing blocked even if a toolbar/menu path has not refreshed its enabled state yet.
// validate_current_plate also refreshes the missing-plugin notifications and plate readiness.
bool model_fits = false;
bool validate_error = false;
validate_current_plate(model_fits, validate_error);
if (!model_fits || validate_error) {
p->main_frame->update_slice_print_status(MainFrame::eEventPlateUpdate, false);
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": current plate validation failed; slicing blocked.";
return;
}
// Stop the running (and queued) UI jobs and only proceed if they actually
// get stopped.
unsigned timeout_ms = 10000;
@@ -17852,12 +17890,212 @@ void Plater::validate_current_plate(bool& model_fits, bool& validate_error)
}*/
}
// Missing-plugin validation (both technologies): block slicing while the active preset(s)
// reference plugin capabilities that are not installed/loadable here. The helper rebuilds the
// missing sets, manages the notifications, and reports whether slicing is blocked. plugins_block_changed
// is set when the block toggled, so the Slice button can be refreshed below.
bool plugins_block_changed = false;
if (refresh_missing_plugin_block(&plugins_block_changed)) {
model_fits = false;
validate_error = true;
}
PartPlate* part_plate = p->partplate_list.get_curr_plate();
part_plate->update_slice_ready_status(model_fits);
part_plate->update_slice_ready_status(model_fits && !validate_error);
// The toolbar Slice button is normally refreshed only by the canvas
// (EVT_GLCANVAS_ENABLE_ACTION_BUTTONS) on geometry updates. When the missing-plugin block toggles
// without a geometry change (e.g. a plugin finishing loading, or a setting edit that drops the
// last missing plugin), refresh it here — AFTER update_slice_ready_status set the plate's
// can_slice() flag that get_enable_slice_status() reads — so the button doesn't lag until the
// next bed click.
if (plugins_block_changed && !p->background_process.running())
p->main_frame->update_slice_print_status(MainFrame::eEventObjectUpdate, model_fits && !validate_error);
return;
}
bool Plater::refresh_missing_plugin_block(bool* block_toggled)
{
// PluginResolver owns the per-preset-type missing sets; rebuild them from each preset's own
// "plugins" manifest so the state is always fresh. A plugin is resolved when it is
// installed/loaded, or when no active setting references it any more (refresh drops it then).
// The block is derived solely from PluginResolver; snapshot it before the refresh to detect a
// toggle (so the caller can refresh the Slice button).
const bool was_blocked = has_missing_plugins() || has_inactive_plugins() || has_broken_plugins();
refresh_missing_plugins(*wxGetApp().preset_bundle);
const auto missing_refs = [](const std::vector<MissingPlugin>& missing) {
std::vector<std::string> refs;
refs.reserve(missing.size());
for (const MissingPlugin& m : missing)
refs.emplace_back(create_full_ref(m.ref));
return refs;
};
const auto signature = [&missing_refs](const std::vector<MissingPlugin>& missing) {
std::vector<std::string> refs = missing_refs(missing);
std::sort(refs.begin(), refs.end());
std::string sig;
for (const std::string& r : refs) { sig += r; sig += '\n'; }
return sig;
};
// Show/refresh the non-closable notification for one missing set. Only (re)create it when the
// set changes; pushing every validate would close+recreate it (flicker, reset hover) since
// validate runs on many triggers. shown_sig also gates plugin-load re-validation.
const auto update = [&](NotificationType type, const std::vector<MissingPlugin>& missing,
std::string* shown_sig, const std::string& header,
const std::string& resolve_label,
std::function<bool(wxEvtHandler*)> resolve_action) {
if (missing.empty()) {
if (!shown_sig->empty()) {
p->notification_manager->close_notification_of_type(type);
shown_sig->clear();
}
return;
}
const std::string sig = signature(missing);
if (*shown_sig != sig) {
std::vector<JumpTo> body;
for (const auto& m : missing)
body.emplace_back(JumpTo{m.ref.capability_name, m.opt, m.opt_type});
p->notification_manager->push_plugin_missing_notification(
type, header, resolve_label, std::move(body), std::move(resolve_action));
*shown_sig = sig;
}
};
const std::vector<MissingPlugin> missing_cloud = get_missing_cloud_plugins();
const std::vector<MissingPlugin> missing_local = get_missing_local_plugins();
const std::vector<std::string> missing_cloud_refs = missing_refs(missing_cloud);
const std::vector<std::string> missing_local_refs = missing_refs(missing_local);
update(NotificationType::OrcaCloudPluginMissingError, missing_cloud,
&p->m_cloud_missing_shown_sig,
_u8L("OrcaCloud plugins required by the current preset are not installed:"),
_u8L("Install Plugins"),
[this, missing_cloud_refs](wxEvtHandler*) { install_missing_cloud_plugins(missing_cloud_refs); return false; });
// "Find on OrcaCloud" is only a suggestion: it opens the browser but cannot resolve the missing
// plugin in-session, so it never closes the notification or unblocks slicing. The user resolves a
// local plugin by installing it or by changing the setting that needs it.
update(NotificationType::OrcaLocalPluginMissingError, missing_local,
&p->m_local_missing_shown_sig,
_u8L("Local plugins required by the current preset are missing:"),
_u8L("Find on OrcaCloud"),
[missing_local_refs](wxEvtHandler*) { open_missing_plugins_on_cloud(missing_local_refs); return false; });
const std::vector<MissingPlugin> inactive = get_inactive_plugins();
const std::vector<MissingPlugin> broken = get_broken_plugins();
const std::vector<std::string> inactive_refs = missing_refs(inactive);
const std::vector<std::string> broken_refs = missing_refs(broken);
update(NotificationType::OrcaPluginInactiveError, inactive,
&p->m_inactive_shown_sig,
_u8L("Plugins required by the current preset are not activated:"),
_u8L("Activate Now"),
[this, inactive_refs](wxEvtHandler*) { enable_inactive_plugins(inactive_refs); return false; });
update(NotificationType::OrcaPluginCapabilityUnavailableError, broken,
&p->m_broken_shown_sig,
_u8L("The installed plugin does not provide the required capability — it may be outdated:"),
_u8L("Find on OrcaCloud"),
[broken_refs](wxEvtHandler*) { open_missing_plugins_on_cloud(broken_refs); return false; });
const bool blocked = has_missing_plugins() || has_inactive_plugins() || has_broken_plugins();
if (block_toggled)
*block_toggled = (was_blocked != blocked);
return blocked;
}
void Plater::revalidate_current_plate_if_plugins_missing()
{
// Only do work while a missing-plugin notification is up, so the plugin-load hook does not
// trigger a full validation for every plugin that loads during normal startup/use.
if (p->m_local_missing_shown_sig.empty() && p->m_cloud_missing_shown_sig.empty() &&
p->m_inactive_shown_sig.empty() && p->m_broken_shown_sig.empty())
return;
bool model_fits = true, validate_error = false;
validate_current_plate(model_fits, validate_error);
}
void Plater::install_missing_cloud_plugins(const std::vector<std::string>& cloud_refs)
{
if (cloud_refs.empty())
return;
// Shared between the UI-thread dialog/timer and the resolver's worker thread.
struct InstallProgressState
{
std::atomic<bool> cancel{false};
std::atomic<bool> finished{false};
std::atomic<bool> torn_down{false};
std::mutex mtx;
std::string message;
};
auto state = std::make_shared<InstallProgressState>();
state->message = _u8L("Preparing to install plugins...");
wxWindow* parent = wxGetApp().mainframe;
auto* dialog = new wxProgressDialog(_L("Installing plugins"), from_u8(state->message), 100,
parent, wxPD_APP_MODAL | wxPD_CAN_ABORT);
dialog->Pulse();
// UI-thread timer: animate the pulse, observe the Cancel button, and tear down when the worker
// signals completion. The timer is deleted via CallAfter so it is never freed inside its own
// handler.
auto* timer = new wxTimer();
timer->Bind(wxEVT_TIMER, [this, dialog, timer, state](wxTimerEvent&) {
std::string msg;
{
std::lock_guard<std::mutex> lock(state->mtx);
msg = state->message;
}
// Once cancellation is requested, the in-flight plugin still has to finish; reflect that.
if (state->cancel)
msg = _u8L("Cancelling — finishing the current plugin...");
if (!dialog->Pulse(from_u8(msg)))
state->cancel = true;
// Tear down exactly once: Stop() prevents further ticks, but guard so a stale queued tick
// can never double-Destroy the dialog or double-delete the timer.
if (state->finished && !state->torn_down.exchange(true)) {
timer->Stop();
dialog->Destroy();
wxGetApp().CallAfter([timer]() { delete timer; });
revalidate_current_plate_if_plugins_missing();
}
});
timer->Start(100);
PluginInstallProgress progress;
progress.on_plugin_begin = [state](const std::string& name, std::size_t /*index*/, std::size_t /*total*/) {
std::lock_guard<std::mutex> lock(state->mtx);
state->message = (boost::format(_u8L("Installing %1%...")) % name).str();
};
progress.is_cancelled = [state]() { return state->cancel.load(); };
progress.on_finished = [state]() { state->finished = true; };
resolve_missing_plugins(cloud_refs, std::move(progress));
}
void Plater::enable_inactive_plugins(const std::vector<std::string>& refs)
{
if (refs.empty())
return;
// Local and instant — load the plugin and/or enable the capability. The plugin-load callback
// re-validates the plate and clears (or reclassifies) the notification; no progress dialog needed.
resolve_inactive_plugins(refs);
}
bool Plater::plugins_block_slicing() const
{
// Single source of truth: slicing is blocked while PluginResolver still has unresolved plugin
// references — missing (download), inactive (activate), or broken (capability unavailable).
return has_missing_plugins() || has_inactive_plugins() || has_broken_plugins();
}
void Plater::open_platesettings_dialog(wxCommandEvent& evt) {
int plate_index = evt.GetInt();
PlateSettingsDialog dlg(this, _L("Plate Settings"), evt.GetString() == "only_layer_sequence");
+19
View File
@@ -707,6 +707,25 @@ public:
//BBS: partplate list related functions
PartPlateList& get_partplate_list();
void validate_current_plate(bool& model_fits, bool& validate_error);
// Rebuild the missing-plugin sets from the active presets and (re)show/close their notifications.
// Returns true when slicing must be blocked (a referenced plugin is still missing); sets
// *block_toggled when the blocked state changed since the previous call (so the caller can refresh
// the Slice button). Helper for validate_current_plate.
bool refresh_missing_plugin_block(bool* block_toggled = nullptr);
// Re-run plate validation when a plugin finishes loading, but only while a missing-plugin
// notification is active, so it clears automatically once the required plugin is available.
void revalidate_current_plate_if_plugins_missing();
// Install the given missing cloud plugin refs, showing an app-modal pulsing progress dialog
// (Cancel stops before the next plugin). Runs the blocking install on a worker thread; the
// dialog is driven from the UI thread. Clears the missing-plugin notification once resolved.
void install_missing_cloud_plugins(const std::vector<std::string>& cloud_refs);
// Activate the given inactive plugin refs (load the plugin / enable the capability). No progress
// dialog; the loads run on a background worker that re-validates the plate once they settle, so
// the notification clears (or flips to broken if the plugin lacks the capability).
void enable_inactive_plugins(const std::vector<std::string>& refs);
// True when the active preset references plugins that are missing and not yet acknowledged, as
// of the last validate_current_plate. Other slice-ready writers consult this to stay consistent.
bool plugins_block_slicing() const;
//BBS: select the plate by index
int select_plate(int plate_index, bool need_slice = false);
//BBS: update progress result
+190
View File
@@ -0,0 +1,190 @@
#include "PluginPickerDialog.hpp"
#include <wx/button.h>
#include <wx/choice.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/valgen.h>
#include "GUI.hpp"
#include "I18N.hpp"
namespace Slic3r { namespace GUI {
PluginPickerDialog::PluginPickerDialog(wxWindow* parent,
const wxString& plugin_type_label,
const std::vector<Slic3r::PluginDescriptor>& plugins)
: wxDialog(parent, wxID_ANY, wxString::Format(_L("Select %s Plugin"), plugin_type_label))
, m_plugins(plugins)
, m_capability_mode(false)
{
build_ui(plugin_type_label);
CentreOnParent();
}
PluginPickerDialog::PluginPickerDialog(wxWindow* parent,
const wxString& plugin_type_label,
std::vector<CapabilityEntry> capabilities)
: wxDialog(parent, wxID_ANY, wxString::Format(_L("Select %s Plugin"), plugin_type_label))
, m_capabilities(std::move(capabilities))
, m_capability_mode(true)
{
build_capability_ui(plugin_type_label);
CentreOnParent();
}
void PluginPickerDialog::build_ui(const wxString& plugin_type_label)
{
const bool has_plugins = !m_plugins.empty();
auto* top_sizer = new wxBoxSizer(wxVERTICAL);
auto* info_text = new wxStaticText(this, wxID_ANY,
wxString::Format(_L("Choose a %s plugin from the list below."), plugin_type_label));
top_sizer->Add(info_text, 0, wxALL | wxEXPAND, 10);
wxArrayString choices;
choices.reserve(m_plugins.size());
for (const auto& plugin : m_plugins) {
wxString label = from_u8(plugin.name);
if (!plugin.version.empty())
label += wxString::Format(" (%s)", from_u8(plugin.version));
choices.Add(label);
}
m_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices);
if (has_plugins) {
m_choice->SetSelection(0);
m_choice->Bind(wxEVT_CHOICE, [this](wxCommandEvent& evt) {
update_description(evt.GetSelection());
});
} else {
m_choice->Enable(false);
}
top_sizer->Add(m_choice, 0, wxLEFT | wxRIGHT | wxEXPAND, 10);
m_description = new wxStaticText(this, wxID_ANY, wxEmptyString);
m_description->Wrap(400);
top_sizer->Add(m_description, 0, wxALL | wxEXPAND, 10);
if (has_plugins)
update_description(0);
else
m_description->SetLabel(_L("No plugins found for this type."));
auto* button_sizer = new wxStdDialogButtonSizer();
auto* ok_button = new wxButton(this, wxID_OK);
ok_button->Enable(has_plugins);
button_sizer->AddButton(ok_button);
button_sizer->AddButton(new wxButton(this, wxID_CANCEL));
button_sizer->Realize();
top_sizer->Add(button_sizer, 0, wxALL | wxALIGN_RIGHT, 10);
SetSizerAndFit(top_sizer);
}
void PluginPickerDialog::build_capability_ui(const wxString& plugin_type_label)
{
const bool has_capabilities = !m_capabilities.empty();
auto* top_sizer = new wxBoxSizer(wxVERTICAL);
auto* info_text = new wxStaticText(this, wxID_ANY,
wxString::Format(_L("Choose a %s plugin from the list below."), plugin_type_label));
top_sizer->Add(info_text, 0, wxALL | wxEXPAND, 10);
wxArrayString choices;
choices.reserve(m_capabilities.size());
for (const auto& cap : m_capabilities)
choices.Add(cap.label);
m_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices);
if (has_capabilities) {
m_choice->SetSelection(0);
m_choice->Bind(wxEVT_CHOICE, [this](wxCommandEvent& evt) {
update_capability_description(evt.GetSelection());
});
} else {
m_choice->Enable(false);
}
top_sizer->Add(m_choice, 0, wxLEFT | wxRIGHT | wxEXPAND, 10);
m_description = new wxStaticText(this, wxID_ANY, wxEmptyString);
m_description->Wrap(400);
top_sizer->Add(m_description, 0, wxALL | wxEXPAND, 10);
if (has_capabilities)
update_capability_description(0);
else
m_description->SetLabel(_L("No plugins found for this type."));
auto* button_sizer = new wxStdDialogButtonSizer();
auto* ok_button = new wxButton(this, wxID_OK);
ok_button->Enable(has_capabilities);
button_sizer->AddButton(ok_button);
button_sizer->AddButton(new wxButton(this, wxID_CANCEL));
button_sizer->Realize();
top_sizer->Add(button_sizer, 0, wxALL | wxALIGN_RIGHT, 10);
SetSizerAndFit(top_sizer);
}
PluginPickerDialog::CapabilityEntry PluginPickerDialog::selected_capability() const
{
if (!m_choice || !m_choice->IsEnabled())
return {};
int sel = m_choice->GetSelection();
if (sel < 0 || static_cast<size_t>(sel) >= m_capabilities.size())
return {};
return m_capabilities[static_cast<size_t>(sel)];
}
void PluginPickerDialog::update_capability_description(int selection)
{
if (!m_description)
return;
if (selection < 0 || static_cast<size_t>(selection) >= m_capabilities.size()) {
m_description->SetLabel(wxEmptyString);
return;
}
const auto& cap = m_capabilities[static_cast<size_t>(selection)];
m_description->SetLabel(cap.description.empty() ? cap.label : cap.description);
m_description->Wrap(400);
Layout();
}
std::string PluginPickerDialog::selected_plugin_key() const
{
if (!m_choice || !m_choice->IsEnabled())
return {};
int selection = m_choice->GetSelection();
if (selection < 0 || static_cast<size_t>(selection) >= m_plugins.size())
return {};
const auto& plugin = m_plugins[static_cast<size_t>(selection)];
return plugin.plugin_key;
}
void PluginPickerDialog::update_description(int selection)
{
if (!m_description)
return;
if (selection < 0 || static_cast<size_t>(selection) >= m_plugins.size()) {
m_description->SetLabel(wxEmptyString);
return;
}
const auto& plugin = m_plugins[static_cast<size_t>(selection)];
wxString desc;
if (!plugin.description.empty())
desc = from_u8(plugin.description);
else
desc = wxString::Format(_L("Plugin file: %s"), from_u8(plugin.entry_path));
m_description->SetLabel(desc);
m_description->Wrap(400);
Layout();
}
}} // namespace Slic3r::GUI
+60
View File
@@ -0,0 +1,60 @@
#ifndef SLIC3R_GUI_PLUGINPICKERDIALOG_HPP
#define SLIC3R_GUI_PLUGINPICKERDIALOG_HPP
#include <wx/wxprec.h>
#ifndef WX_PRECOMP
# include <wx/wx.h>
#endif
#include <vector>
#include <string>
#include "slic3r/plugin/PluginManager.hpp"
namespace Slic3r { namespace GUI {
class PluginPickerDialog : public wxDialog
{
public:
// Entry for capability-level selection (plugin_type non-empty path).
struct CapabilityEntry {
std::string plugin_key;
std::string name;
wxString label;
wxString description;
};
// Existing constructor: offer all loaded plugin packages (plugin_type empty path).
PluginPickerDialog(wxWindow* parent,
const wxString& plugin_type_label,
const std::vector<Slic3r::PluginDescriptor>& plugins);
// New constructor: offer a list of capabilities (plugin_type non-empty path).
PluginPickerDialog(wxWindow* parent,
const wxString& plugin_type_label,
std::vector<CapabilityEntry> capabilities);
// Returns the plugin_key of the selected plugin package (package path).
std::string selected_plugin_key() const;
// Returns the {plugin_key, name} of the selected capability (capability path).
CapabilityEntry selected_capability() const;
void update_plugin_list(const std::vector<PluginDescriptor>& updated_plugins) { m_plugins = updated_plugins; }
private:
void build_ui(const wxString& plugin_type_label);
void build_capability_ui(const wxString& plugin_type_label);
void update_description(int selection);
void update_capability_description(int selection);
wxChoice* m_choice { nullptr };
wxStaticText* m_description { nullptr };
std::vector<Slic3r::PluginDescriptor> m_plugins;
std::vector<CapabilityEntry> m_capabilities;
bool m_capability_mode { false };
};
}} // namespace Slic3r::GUI
#endif // SLIC3R_GUI_PLUGINPICKERDIALOG_HPP
+134
View File
@@ -0,0 +1,134 @@
#include "PluginProgressDialog.hpp"
#include <utility>
namespace Slic3r { namespace GUI {
PluginProgressDialog::PluginProgressDialog(wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
CloseHandler on_destroyed)
: wxProgressDialog(title, message, maximum, parent, style)
, m_pulse_message(message)
, m_on_destroyed(std::move(on_destroyed))
{
Bind(wxEVT_CLOSE_WINDOW, &PluginProgressDialog::on_close_window, this);
}
PluginProgressDialog::~PluginProgressDialog()
{
stop_pulse();
if (m_on_destroyed)
m_on_destroyed();
}
PluginProgressDialog* PluginProgressDialog::create_dialog(wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
CloseHandler on_destroyed)
{
return new PluginProgressDialog(parent, title, message, maximum, style, std::move(on_destroyed));
}
bool PluginProgressDialog::pulse(PluginProgressDialog* dialog, const wxString& message)
{
return dialog != nullptr ? dialog->pulse(message) : false;
}
bool PluginProgressDialog::update(PluginProgressDialog* dialog, int value, const wxString& message)
{
return dialog != nullptr ? dialog->update(value, message) : false;
}
void PluginProgressDialog::start_pulse(PluginProgressDialog* dialog, int interval_ms, const wxString& message)
{
if (dialog != nullptr)
dialog->start_pulse(interval_ms, message);
}
void PluginProgressDialog::stop_pulse(PluginProgressDialog* dialog)
{
if (dialog != nullptr)
dialog->stop_pulse();
}
void PluginProgressDialog::request_close(PluginProgressDialog* dialog)
{
if (dialog != nullptr)
dialog->close();
}
bool PluginProgressDialog::pulse(const wxString& message)
{
if (!m_open)
return false;
if (!message.empty())
m_pulse_message = message;
return Pulse(message);
}
bool PluginProgressDialog::update(int value, const wxString& message)
{
if (!m_open)
return false;
if (!message.empty())
m_pulse_message = message;
return Update(value, message);
}
void PluginProgressDialog::start_pulse(int interval_ms, const wxString& message)
{
if (!m_open)
return;
if (!message.empty())
m_pulse_message = message;
stop_pulse();
m_timer = std::make_unique<wxTimer>(this);
Bind(wxEVT_TIMER, &PluginProgressDialog::on_timer, this, m_timer->GetId());
m_timer->Start(interval_ms > 0 ? interval_ms : 1);
}
void PluginProgressDialog::stop_pulse()
{
if (!m_timer)
return;
Unbind(wxEVT_TIMER, &PluginProgressDialog::on_timer, this, m_timer->GetId());
m_timer->Stop();
m_timer.reset();
}
void PluginProgressDialog::close()
{
if (!m_open)
return;
m_open = false;
stop_pulse();
Destroy();
}
void PluginProgressDialog::on_close_window(wxCloseEvent& /*event*/)
{
close();
}
void PluginProgressDialog::on_timer(wxTimerEvent& /*event*/)
{
if (m_open)
Pulse(m_pulse_message);
}
}} // namespace Slic3r::GUI
+66
View File
@@ -0,0 +1,66 @@
#ifndef slic3r_GUI_PluginProgressDialog_hpp_
#define slic3r_GUI_PluginProgressDialog_hpp_
#include <functional>
#include <memory>
#include <wx/event.h>
#include <wx/progdlg.h>
#include <wx/string.h>
#include <wx/timer.h>
#include <wx/window.h>
namespace Slic3r { namespace GUI {
// A host-owned progress dialog for Python plugins. This class is deliberately
// Python-agnostic; the plugin layer owns any pybind/GIL concerns and marshals
// all calls to the UI thread.
class PluginProgressDialog : public wxProgressDialog
{
public:
using CloseHandler = std::function<void()>;
// on_destroyed runs from the destructor on every path and must touch
// host-side state only (no Python / no derived members).
PluginProgressDialog(wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
CloseHandler on_destroyed = nullptr);
~PluginProgressDialog() override;
// Convenience helpers for plugin-host callers. MAIN-THREAD ONLY.
static PluginProgressDialog* create_dialog(wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
CloseHandler on_destroyed);
static bool pulse(PluginProgressDialog* dialog, const wxString& message = wxEmptyString);
static bool update(PluginProgressDialog* dialog, int value, const wxString& message = wxEmptyString);
static void start_pulse(PluginProgressDialog* dialog, int interval_ms, const wxString& message = wxEmptyString);
static void stop_pulse(PluginProgressDialog* dialog);
static void request_close(PluginProgressDialog* dialog);
// MAIN-THREAD ONLY.
bool pulse(const wxString& message = wxEmptyString);
bool update(int value, const wxString& message = wxEmptyString);
void start_pulse(int interval_ms, const wxString& message = wxEmptyString);
void stop_pulse();
void close();
bool is_open() const { return m_open; }
private:
void on_close_window(wxCloseEvent& event);
void on_timer(wxTimerEvent& event);
bool m_open{true};
wxString m_pulse_message;
std::unique_ptr<wxTimer> m_timer;
CloseHandler m_on_destroyed;
};
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_PluginProgressDialog_hpp_
+318
View File
@@ -0,0 +1,318 @@
#include "PluginWebDialog.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include <libslic3r/Utils.hpp>
#include <boost/filesystem.hpp>
#include <wx/event.h>
#include <utility>
namespace Slic3r { namespace GUI {
namespace {
// CSS "#rrggbb" for a wxColour (wxC2S_HTML_SYNTAX is the portable accessor used
// throughout the codebase for color->CSS).
std::string css_color(const wxColour& c) { return c.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); }
// Build a <style> block that matches OrcaSlicer's current theme. Injected at
// document-start (see ctor) so an unstyled plugin page already looks native,
// while plugin CSS still wins:
// * variables live on :root and elements use only low-specificity selectors,
// * nothing is marked !important,
// so any later rule the plugin ships (even an element selector) overrides these.
// Generated in C++ from the live theme — correct for the active light/dark mode
// and accent without the page needing to detect anything.
std::string host_theme_style()
{
GUI_App& app = wxGetApp();
const wxColour bg = app.get_window_default_clr(); // dialog background
const wxColour fg = app.get_label_clr_default(); // primary text
const wxColour muted = app.get_label_clr_sys(); // secondary text
const wxColour border = app.get_highlight_default_clr(); // subtle lines / row hover
const wxColour accent = StateColor::darkModeColorFor(wxColour("#009688")); // ORCA teal
const wxColour accent_fg = *wxWHITE;
const std::string font = app.normal_font().GetFaceName().ToStdString();
std::string s;
s += "<style id=\"orca-host-theme\">";
s += ":root{";
s += "--orca-bg:" + css_color(bg) + ";";
s += "--orca-fg:" + css_color(fg) + ";";
s += "--orca-muted:" + css_color(muted) + ";";
s += "--orca-border:" + css_color(border) + ";";
s += "--orca-accent:" + css_color(accent) + ";";
s += "--orca-accent-fg:" + css_color(accent_fg) + ";";
// The themed face name first, then a portable system-ui fallback stack.
s += "--orca-font:" + (font.empty() ? std::string() : "'" + font + "',") +
"system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;";
s += "color-scheme:" + std::string(app.dark_mode() ? "dark" : "light") + ";";
s += "}";
// Element defaults — low specificity, no !important.
s += "html,body{background:var(--orca-bg);color:var(--orca-fg);"
"font-family:var(--orca-font);font-size:13px;}";
s += "body{margin:0;}";
s += "h1,h2,h3,h4,h5,h6{color:var(--orca-fg);font-weight:600;}";
s += "a{color:var(--orca-accent);}";
s += "hr{border:0;border-top:1px solid var(--orca-border);}";
s += "button{font:inherit;color:var(--orca-accent-fg);background:var(--orca-accent);"
"border:1px solid var(--orca-accent);border-radius:4px;padding:5px 14px;cursor:pointer;}";
s += "button:hover{filter:brightness(1.1);}";
s += "button:disabled{opacity:.5;cursor:default;}";
s += "input,select,textarea{font:inherit;color:var(--orca-fg);"
"background:var(--orca-bg);border:1px solid var(--orca-border);"
"border-radius:4px;padding:4px 8px;}";
s += "input:focus,select:focus,textarea:focus{outline:none;border-color:var(--orca-accent);}";
s += "table{border-collapse:collapse;}";
s += "th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--orca-border);}";
s += "th{color:var(--orca-muted);font-weight:600;}";
// WebKit/Chromium scrollbars themed to the background (no-op on others).
s += "::-webkit-scrollbar{width:12px;height:12px;}";
s += "::-webkit-scrollbar-thumb{background:var(--orca-border);border-radius:6px;}";
s += "::-webkit-scrollbar-track{background:transparent;}";
s += "</style>";
return s;
}
// User script that prepends the host theme into the document at document-start,
// before the plugin's own <style>/scripts run (and before first paint). Works
// whether the plugin page has a <head> or is a bare fragment.
std::string host_theme_user_script()
{
// JSON-encode the style so it is a safe JS string literal regardless of
// quotes/newlines it may contain.
const std::string style_literal = nlohmann::json(host_theme_style()).dump();
std::string js;
js += "(function(){";
js += "if(document.getElementById('orca-host-theme'))return;";
js += "var css=" + style_literal + ";";
js += "var head=document.head||document.documentElement;";
js += "head.insertAdjacentHTML('afterbegin',css);";
js += "})();";
return js;
}
// 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.
constexpr char ORCA_BRIDGE_JS[] = R"JS(
(function () {
if (window.orca) return;
var handlers = [];
function send(kind, data) {
try {
window.wx.postMessage(JSON.stringify({
channel: 'orca', kind: kind, data: (data === undefined ? null : data)
}));
} catch (e) { /* bridge not ready yet */ }
}
window.orca = {
postMessage: function (d) { send('message', d); },
submit: function (d) { send('submit', d); },
close: function () { send('close'); },
onMessage: function (cb) { if (typeof cb === 'function') handlers.push(cb); }
};
window.__orcaDispatch = function (payload) {
var data = payload ? payload.data : null;
for (var i = 0; i < handlers.length; i++) {
try { handlers[i](data); } catch (e) {}
}
};
})();
)JS";
// file:// base URL for plugin HTML loaded via SetPage, so self-referencing
// relative URLs resolve against the bundled web resources directory.
wxString web_base_url()
{
const std::string dir = (boost::filesystem::path(resources_dir()) / "web").make_preferred().string();
return wxString("file://") + from_u8(dir) + "/";
}
} // namespace
PluginWebDialog::PluginWebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
CloseHandler on_close,
CloseHandler on_destroyed)
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size)
, m_html(html)
, m_on_message(std::move(on_message))
, m_on_close(std::move(on_close))
, m_on_destroyed(std::move(on_destroyed))
{
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML
// is swapped in via SetPage once the bootstrap finishes loading.
create_webview("web/dialog/PluginWebDialog/blank.html", title, size, wxSize(320, 240));
// Paint the window/webview in the themed background so there is no white
// flash before the (transparent) bootstrap page and plugin HTML render.
SetBackgroundColour(wxGetApp().get_window_default_clr());
if (wxWebView* wv = browser()) {
wv->SetBackgroundColour(wxGetApp().get_window_default_clr());
// Inject the host theme first so its <style> sits ahead of any plugin
// CSS in the document (later same-specificity rules win), making the
// plugin page match OrcaSlicer's light/dark theme by default.
wv->AddUserScript(wxString::FromUTF8(host_theme_user_script()));
wv->AddUserScript(wxString::FromUTF8(ORCA_BRIDGE_JS));
// Swap in the plugin HTML once the bootstrap page settles. Bind ERROR too so a
// missing/blocked bootstrap resource (e.g. a packaged build) still triggers it.
Bind(wxEVT_WEBVIEW_LOADED, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
Bind(wxEVT_WEBVIEW_ERROR, &PluginWebDialog::on_bootstrap_event, this, wv->GetId());
}
Bind(wxEVT_CLOSE_WINDOW, &PluginWebDialog::on_close_window, this);
}
PluginWebDialog::~PluginWebDialog()
{
// Runs on every destruction path. Deliberately NOT a wxEVT_DESTROY handler:
// that event is sent from the base ~wxDialog(), after this subclass's members
// are already destroyed. Here the members are still alive, and the callback
// only touches the host-side registry (no Python), so this is safe.
if (m_on_destroyed)
m_on_destroyed();
}
std::optional<nlohmann::json> PluginWebDialog::show_modal_dialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message)
{
PluginWebDialog dlg(parent, title, html, size, std::move(on_message), nullptr, nullptr);
dlg.ShowModal();
return dlg.result();
}
PluginWebDialog* PluginWebDialog::create_modeless_dialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
CloseHandler on_close,
CloseHandler on_destroyed)
{
return new PluginWebDialog(parent, title, html, size, std::move(on_message), std::move(on_close),
std::move(on_destroyed));
}
void PluginWebDialog::show_modeless_dialog(PluginWebDialog* dialog)
{
if (dialog == nullptr)
return;
dialog->Show();
dialog->Raise();
}
void PluginWebDialog::post_message(PluginWebDialog* dialog, const nlohmann::json& data)
{
if (dialog != nullptr && dialog->is_open())
dialog->push_message(data);
}
void PluginWebDialog::request_close(PluginWebDialog* dialog)
{
if (dialog != nullptr)
dialog->Close();
}
void PluginWebDialog::on_bootstrap_event(wxWebViewEvent& event)
{
// The first bootstrap load (or its error) triggers the swap to plugin HTML;
// the resulting plugin-page load is ignored (guarded by m_content_loaded).
load_plugin_content();
event.Skip();
}
void PluginWebDialog::load_plugin_content()
{
if (m_content_loaded)
return;
m_content_loaded = true;
if (wxWebView* wv = browser())
wv->SetPage(wxString::FromUTF8(m_html), web_base_url());
}
void PluginWebDialog::on_script_message(const nlohmann::json& payload)
{
if (payload.value("channel", std::string()) == "orca") {
const std::string kind = payload.value("kind", std::string());
const nlohmann::json data = payload.contains("data") ? payload["data"] : nlohmann::json();
if (kind == "message") {
if (m_on_message)
m_on_message(data);
} else if (kind == "submit") {
finish(true, data);
} else if (kind == "close") {
finish(false, nlohmann::json());
}
return;
}
// Fall back to the shared shell commands (e.g. "close_page").
handle_common_script_command(payload);
}
void PluginWebDialog::push_message(const nlohmann::json& data)
{
if (!m_open)
return;
nlohmann::json envelope;
envelope["data"] = data;
call_web_handler(envelope, wxT("__orcaDispatch"));
}
void PluginWebDialog::finish(bool submitted, const nlohmann::json& data)
{
if (!m_open)
return;
m_open = false;
if (submitted)
m_result = data;
else
m_result.reset();
if (IsModal())
EndModal(submitted ? wxID_OK : wxID_CANCEL);
else
Close();
}
void PluginWebDialog::on_close_window(wxCloseEvent&)
{
m_open = false;
if (IsModal()) {
EndModal(m_result.has_value() ? wxID_OK : wxID_CANCEL);
return;
}
// Modeless: the window is still fully alive here (unlike in wxEVT_DESTROY), so
// it is safe to invoke the plugin's on_close before destroying the window.
fire_close();
Destroy();
}
void PluginWebDialog::fire_close()
{
if (m_close_fired)
return;
m_close_fired = true;
if (m_on_close) {
CloseHandler cb = m_on_close;
m_on_close = nullptr;
cb();
}
}
}} // namespace Slic3r::GUI
+92
View File
@@ -0,0 +1,92 @@
#ifndef slic3r_GUI_PluginWebDialog_hpp_
#define slic3r_GUI_PluginWebDialog_hpp_
#include "Widgets/WebViewHostDialog.hpp"
#include <functional>
#include <optional>
#include <string>
#include <nlohmann/json.hpp>
#include <wx/webview.h>
namespace Slic3r { namespace GUI {
// A host-owned webview window that renders plugin-supplied raw HTML and bridges
// messages to/from the page through a small injected `window.orca` API.
//
// This class is deliberately Python-agnostic: it talks to the plugin layer only
// through std::function hooks. Those hooks must NOT capture bare pybind11
// objects, because the dialog can be destroyed on the main thread without the
// GIL held; the plugin layer wraps any Python callables in a GIL-safe holder.
//
// Usable both modally (ShowModal -> read result()) and modelessly (Show()).
class PluginWebDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
using MessageHandler = std::function<void(const nlohmann::json& data)>;
using CloseHandler = std::function<void()>;
// on_close fires only on a user/JS-initiated close (while the window is
// alive). on_destroyed runs from the destructor on every path and must touch
// host-side state only (no Python / no derived members).
PluginWebDialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
CloseHandler on_close,
CloseHandler on_destroyed);
~PluginWebDialog() override;
// Convenience helpers for plugin-host callers. MAIN-THREAD ONLY.
static std::optional<nlohmann::json> show_modal_dialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message);
static PluginWebDialog* create_modeless_dialog(wxWindow* parent,
const wxString& title,
const std::string& html,
const wxSize& size,
MessageHandler on_message,
CloseHandler on_close,
CloseHandler on_destroyed);
static void show_modeless_dialog(PluginWebDialog* dialog);
static void post_message(PluginWebDialog* dialog, const nlohmann::json& data);
static void request_close(PluginWebDialog* dialog);
// Push a payload to the page; delivered to handlers registered via
// window.orca.onMessage(). MAIN-THREAD ONLY (the plugin layer marshals).
void push_message(const nlohmann::json& data);
bool is_open() const { return m_open; }
// The payload submitted via window.orca.submit() (modal use), if any.
const std::optional<nlohmann::json>& result() const { return m_result; }
protected:
void on_script_message(const nlohmann::json& payload) override;
// Plugin HTML is loaded as a raw string, not a localized resource URL.
bool append_language_to_url() const override { return false; }
private:
void on_bootstrap_event(wxWebViewEvent& event);
void load_plugin_content();
void on_close_window(wxCloseEvent& event);
void fire_close();
void finish(bool submitted, const nlohmann::json& data);
std::string m_html;
bool m_content_loaded{false};
bool m_open{true};
bool m_close_fired{false};
std::optional<nlohmann::json> m_result;
MessageHandler m_on_message;
CloseHandler m_on_close;
CloseHandler m_on_destroyed;
};
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_PluginWebDialog_hpp_
File diff suppressed because it is too large. Load diff
+226
View File
@@ -0,0 +1,226 @@
#ifndef slic3r_PluginsDialog_hpp_
#define slic3r_PluginsDialog_hpp_
#include "Widgets/WebViewHostDialog.hpp"
#include "slic3r/plugin/PluginDescriptor.hpp"
#include <exception>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <type_traits>
#include <utility>
#include <wx/evtloop.h>
#include <wx/progdlg.h>
#include <wx/string.h>
#include <wx/timer.h>
class wxTimer;
namespace Slic3r {
struct LoadedPluginCapability;
enum class PluginCapabilityType;
namespace GUI {
enum class PluginSource
{
Local,
Mine,
Subscribed
};
enum class PluginStatus
{
Inactive,
Error,
Loading,
Activated
};
class PluginsDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
PluginsDialog(wxWindow* parent,
wxWindowID id = wxID_ANY,
const wxString& title = wxT(""),
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX);
~PluginsDialog();
void set_open_terminal_dlg_fn();
void update_plugin_dialog_ui();
private:
void open_plugin_on_cloud(const std::string& sharing_token);
void open_plugin_hub();
void on_script_message(const nlohmann::json& payload) override;
void send_plugins();
nlohmann::json build_plugins_payload() const;
bool get_descriptor(const std::string& plugin_key, Slic3r::PluginDescriptor& descriptor) const;
std::shared_ptr<LoadedPluginCapability> get_capability(const std::string& plugin_key, PluginCapabilityType type, const std::string& capability_name) const;
void refresh_plugin_catalog_async(const wxString& title, const wxString& message, bool fetch_cloud);
void refresh_plugins();
void toggle_plugin(const std::string& plugin_key, bool enabled);
void toggle_plugin_capability(const std::string& plugin_key, PluginCapabilityType type, const std::string& capability_name, bool enabled);
void handle_plugin_menu_action(const std::string& plugin_key, const std::string& action);
void install_plugin_from_file();
bool install_plugin_package(const std::string& package_path);
bool install_cloud_plugin(const std::string& uuid, const std::string& version, const wxString& name);
void run_script_plugin(const std::string& plugin_key, const std::string& capability_name);
void update_plugin(const std::string& plugin_key);
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 reinstall_cloud_plugin(const Slic3r::PluginDescriptor& plugin);
void delete_mine_local_and_cloud_plugin(const std::string& plugin_key);
// In the future, we can allow users to choose which plugin version they want to install.
template<typename Run, typename OnFinish>
void run_with_dialog(Run&& run,
OnFinish&& on_finish,
const wxString& title,
const wxString& message,
int maximum = 100,
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
{
wxProgressDialog* progress = new wxProgressDialog(title, message, maximum, this, style);
wxTimer* timer = new wxTimer();
timer->Bind(wxEVT_TIMER, [progress, message](wxTimerEvent&) {
if (progress)
progress->Pulse(message);
});
timer->Start(100);
std::thread([this,
progress,
timer,
run = std::forward<Run>(run),
on_finish = std::forward<OnFinish>(on_finish)]() mutable {
try {
run();
} catch (...) {
}
CallAfter([progress, timer, on_finish = std::move(on_finish)]() mutable {
timer->Stop();
delete timer;
progress->Destroy();
on_finish();
});
}).detach();
}
template<typename Run>
std::invoke_result_t<std::decay_t<Run>&> run_with_dialog_wait(Run&& run,
const wxString& title,
const wxString& message,
int maximum = 100,
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
{
using Result = std::invoke_result_t<std::decay_t<Run>&>;
bool finished = false;
wxEventLoop loop;
auto on_finish = [&finished, &loop]() {
finished = true;
if (loop.IsRunning())
loop.Exit();
};
if constexpr (std::is_void_v<Result>) {
struct WaitState
{
std::mutex mutex;
std::exception_ptr exception;
};
auto state = std::make_shared<WaitState>();
run_with_dialog(
[run = std::forward<Run>(run), state]() mutable {
try {
run();
} catch (...) {
std::lock_guard<std::mutex> lock(state->mutex);
state->exception = std::current_exception();
}
},
on_finish, title, message, maximum, style);
if (!finished)
loop.Run();
std::exception_ptr exception;
{
std::lock_guard<std::mutex> lock(state->mutex);
exception = state->exception;
}
if (exception)
std::rethrow_exception(exception);
} else {
using StoredResult = std::decay_t<Result>;
struct WaitState
{
std::mutex mutex;
std::optional<StoredResult> result;
std::exception_ptr exception;
};
auto state = std::make_shared<WaitState>();
run_with_dialog(
[run = std::forward<Run>(run), state]() mutable {
try {
StoredResult result = run();
std::lock_guard<std::mutex> lock(state->mutex);
state->result.emplace(std::move(result));
} catch (...) {
std::lock_guard<std::mutex> lock(state->mutex);
state->exception = std::current_exception();
}
},
on_finish, title, message, maximum, style);
if (!finished)
loop.Run();
std::optional<StoredResult> result;
std::exception_ptr exception;
{
std::lock_guard<std::mutex> lock(state->mutex);
if (state->result)
result.emplace(std::move(*state->result));
exception = state->exception;
}
if (exception)
std::rethrow_exception(exception);
return std::move(*result);
}
}
std::function<void()> m_open_terminal_dlg_fn;
// Serializes run_script_plugin. With main-thread execution a plugin's orca.host.ui modal
// (message/show_dialog) or the result message box pumps a nested event loop, which could
// re-dispatch the web "run_script_plugin" command and start a second, overlapping run.
bool m_script_running = false;
};
} // namespace GUI
} // namespace Slic3r
#endif
+484
View File
@@ -0,0 +1,484 @@
#include "PostProcessor.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r_version.h"
#include "I18N.hpp"
// Post-processing plugins are executed through the embedded-Python plugin system, which is why this
// file lives in the GUI layer (libslic3r must not depend on pybind11 / PluginManager).
#include "libslic3r/Config.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/pluginTypes/gcode/GCodePluginCapability.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
#include <boost/algorithm/string.hpp>
#include <boost/log/trivial.hpp>
#include <boost/format.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/cstdlib.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/nowide/fstream.hpp>
// BBS
#include <algorithm>
#include <iostream>
#include <fstream>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#ifdef WIN32
// The standard Windows includes.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <shellapi.h>
// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
// This routine appends the given argument to a command line such that CommandLineToArgvW will return the argument string unchanged.
// Arguments in a command line should be separated by spaces; this function does not add these spaces.
// Argument - Supplies the argument to encode.
// CommandLine - Supplies the command line to which we append the encoded argument string.
static void quote_argv_winapi(const std::wstring& argument, std::wstring& commmand_line_out)
{
// Don't quote unless we actually need to do so --- hopefully avoid problems if programs won't parse quotes properly.
if (argument.empty() == false && argument.find_first_of(L" \t\n\v\"") == argument.npos)
commmand_line_out.append(argument);
else {
commmand_line_out.push_back(L'"');
for (auto it = argument.begin();; ++it) {
unsigned number_backslashes = 0;
while (it != argument.end() && *it == L'\\') {
++it;
++number_backslashes;
}
if (it == argument.end()) {
// Escape all backslashes, but let the terminating double quotation mark we add below be interpreted as a metacharacter.
commmand_line_out.append(number_backslashes * 2, L'\\');
break;
} else if (*it == L'"') {
// Escape all backslashes and the following double quotation mark.
commmand_line_out.append(number_backslashes * 2 + 1, L'\\');
commmand_line_out.push_back(*it);
} else {
// Backslashes aren't special here.
commmand_line_out.append(number_backslashes, L'\\');
commmand_line_out.push_back(*it);
}
}
commmand_line_out.push_back(L'"');
}
}
static DWORD execute_process_winapi(const std::wstring& command_line)
{
// Extract the current environment to be passed to the child process.
std::wstring envstr;
{
wchar_t* env = GetEnvironmentStrings();
assert(env != nullptr);
const wchar_t* var = env;
size_t totallen = 0;
size_t len;
while ((len = wcslen(var)) > 0) {
totallen += len + 1;
var += len + 1;
}
envstr = std::wstring(env, totallen);
FreeEnvironmentStrings(env);
}
STARTUPINFOW startup_info;
memset(&startup_info, 0, sizeof(startup_info));
startup_info.cb = sizeof(STARTUPINFO);
#if 0
startup_info.dwFlags = STARTF_USESHOWWINDOW;
startup_info.wShowWindow = SW_HIDE;
#endif
PROCESS_INFORMATION process_info;
if (!::CreateProcessW(nullptr /* lpApplicationName */, (LPWSTR) command_line.c_str(), nullptr /* lpProcessAttributes */,
nullptr /* lpThreadAttributes */, false /* bInheritHandles */,
CREATE_UNICODE_ENVIRONMENT /* | CREATE_NEW_CONSOLE */ /* dwCreationFlags */, (LPVOID) envstr.c_str(),
nullptr /* lpCurrentDirectory */, &startup_info, &process_info))
throw Slic3r::RuntimeError(std::string("Failed starting the script ") + boost::nowide::narrow(command_line) +
", Win32 error: " + std::to_string(int(::GetLastError())));
::WaitForSingleObject(process_info.hProcess, INFINITE);
ULONG rc = 0;
::GetExitCodeProcess(process_info.hProcess, &rc);
::CloseHandle(process_info.hThread);
::CloseHandle(process_info.hProcess);
return rc;
}
// Run the script. If it is a perl script, run it through the bundled perl interpreter.
// If it is a batch file, run it through the cmd.exe.
// Otherwise run it directly.
static int run_script(const std::string& script, const std::string& gcode, std::string& /*std_err*/)
{
// Unpack the argument list provided by the user.
int nArgs;
LPWSTR* szArglist = CommandLineToArgvW(boost::nowide::widen(script).c_str(), &nArgs);
if (szArglist == nullptr || nArgs <= 0) {
// CommandLineToArgvW failed. Maybe the command line escapment is invalid?
throw Slic3r::RuntimeError(std::string("Post processing script ") + script + " on file " + gcode +
" failed. CommandLineToArgvW() refused to parse the command line path.");
}
std::wstring command_line;
std::wstring command = szArglist[0];
if (!boost::filesystem::exists(boost::filesystem::path(command)))
throw Slic3r::RuntimeError(std::string("The configured post-processing script does not exist: ") + boost::nowide::narrow(command));
if (boost::iends_with(command, L".pl")) {
// This is a perl script. Run it through the perl interpreter.
// The current process may be slic3r.exe or slic3r-console.exe.
// Find the path of the process:
wchar_t wpath_exe[_MAX_PATH + 1];
::GetModuleFileNameW(nullptr, wpath_exe, _MAX_PATH);
boost::filesystem::path path_exe(wpath_exe);
boost::filesystem::path path_perl = path_exe.parent_path() / "perl" / "perl.exe";
if (!boost::filesystem::exists(path_perl)) {
LocalFree(szArglist);
throw Slic3r::RuntimeError(std::string("Perl interpreter ") + path_perl.string() + " does not exist.");
}
// Replace it with the current perl interpreter.
quote_argv_winapi(boost::nowide::widen(path_perl.string()), command_line);
command_line += L" ";
} else if (boost::iends_with(command, ".bat")) {
// Run a batch file through the command line interpreter.
command_line = L"cmd.exe /C ";
}
for (int i = 0; i < nArgs; ++i) {
quote_argv_winapi(szArglist[i], command_line);
command_line += L" ";
}
LocalFree(szArglist);
quote_argv_winapi(boost::nowide::widen(gcode), command_line);
return (int) execute_process_winapi(command_line);
}
#else
// POSIX
#include <cstdlib> // getenv()
#include <boost/process.hpp>
namespace process = boost::process;
static int run_script(const std::string& script, const std::string& gcode, std::string& std_err)
{
// Try to obtain user's default shell
const char* shell = ::getenv("SHELL");
if (shell == nullptr) {
shell = "/bin/sh";
}
// Quote and escape the gcode path argument
std::string command{script};
command.append(" '");
for (char c : gcode) {
if (c == '\'') {
command.append("'\\''");
} else {
command.push_back(c);
}
}
command.push_back('\'');
BOOST_LOG_TRIVIAL(debug) << boost::format("Executing script, shell: %1%, command: %2%") % shell % command;
process::ipstream istd_err;
process::child child(shell, "-c", command, process::std_err > istd_err);
std_err.clear();
std::string line;
while (child.running() && std::getline(istd_err, line)) {
std_err.append(line);
std_err.push_back('\n');
}
child.wait();
return child.exit_code();
}
#endif
namespace Slic3r {
// BBS
void gcode_add_line_number(const std::string& path, const DynamicPrintConfig& config)
{
const ConfigOptionBool* opt = config.opt<ConfigOptionBool>("gcode_add_line_number");
if (!opt->getBool())
return;
auto gcode_file = boost::filesystem::path(path);
if (!boost::filesystem::exists(gcode_file))
return;
std::fstream fs;
std::string new_gcode;
fs.open(gcode_file.c_str(), std::fstream::in | std::fstream::out);
size_t line_number = 1;
std::string gcode_line;
while (std::getline(fs, gcode_line)) {
char num_str[128];
memset(num_str, 0, sizeof(num_str));
snprintf(num_str, sizeof(num_str), "%zd", line_number);
new_gcode += std::string("N") + num_str + " " + gcode_line + "\n";
line_number++;
}
fs.clear();
fs.seekp(0, std::ios_base::beg);
fs.write(new_gcode.c_str(), new_gcode.length());
fs.close();
}
// Run the configured post-processing plugins on `gcode_path` in place. Plugins are executed in-process
// through the embedded Python interpreter. Throws Slic3r::RuntimeError on any failure; the caller is
// responsible for removing the working copy (see run_post_process_scripts' catch block).
// Entries are bare capability names; the top-level plugins manifest carries the full plugin refs.
static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
const ConfigOptionStrings* plugins,
const std::string& gcode_path,
const std::string& host,
const std::string& output_name)
{
// Let plugins observe the (possibly script-updated) target file name, mirroring the script env.
boost::nowide::setenv("SLIC3R_PP_OUTPUT_NAME", output_name.c_str(), 1);
const boost::filesystem::path gcode_file(gcode_path);
auto execute_fn = [&](std::shared_ptr<GCodePluginCapability> cap, const PluginCapabilityRef& ref) {
GCodePluginContext ctx;
ctx.orca_version = SoftFever_VERSION;
ctx.gcode_path = gcode_path;
ctx.host = host;
ctx.output_name = output_name;
ExecutionResult exec_result;
try {
PythonGILState gil;
exec_result = cap->execute(ctx);
} catch (const std::exception& ex) {
const std::string msg =
(boost::format("Post-processing plugin %1% raised an exception.\nError: %2%") % ref.capability_name % ex.what()).str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
if (exec_result.status == PluginResult::RecoverableError || exec_result.status == PluginResult::FatalError) {
const std::string msg =
(boost::format("Post-processing plugin %1% failed.\nError: %2%") % ref.capability_name % exec_result.message).str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
if (!exec_result.message.empty())
BOOST_LOG_TRIVIAL(info) << "Post-processing plugin " << ref.capability_name << ": " << exec_result.message;
if (!boost::filesystem::exists(gcode_file)) {
const std::string msg = (boost::format(
_utf8(L("Post-processing plugin %1% failed.\n\n"
"The post-processing plugin is expected to modify the G-code file %2% in place, but "
"the G-code file was deleted.\nPlease check the plugin implementation.\n"))) %
ref.capability_name % gcode_path)
.str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
BOOST_LOG_TRIVIAL(info) << "Post-processing plugin " << ref.capability_name << " completed successfully";
};
execute_capabilities_from_refs<GCodePluginCapability>(capabilities, plugins, PluginCapabilityType::PostProcessing, execute_fn);
}
// Run post-processing scripts ("post_process") and/or post-processing plugins ("post_process_plugin")
// if defined. Both run on the same working copy of the G-code (the ".pp" temp when make_copy), so a
// plugin never opens the original file the G-code viewer keeps memory-mapped (a writable open of the
// mapped file fails on Windows with a sharing violation).
// Returns true if a script or plugin was executed.
// Returns false if neither a post-processing script nor plugin was defined.
// Throws an exception on error.
// host is one of "File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ...
// For a "File" target, a temp file will be created for src_path by adding a ".pp" suffix and src_path will be updated.
// In that case the caller is responsible to delete the temp file created.
// output_name is the final name of the G-code on SD card or when uploaded to PrusaLink or OctoPrint.
// If uploading to PrusaLink or OctoPrint, then the file will be renamed to output_name first on the target host.
// The post-processing script may change the output_name.
bool run_post_process_scripts(
std::string& src_path, bool make_copy, const std::string& host, std::string& output_name, const DynamicPrintConfig& config)
{
// post_process / post_process_plugin are absent in SLA mode, hence the null checks.
const auto* post_process = config.opt<ConfigOptionStrings>("post_process");
const auto* post_process_plugin = config.opt<ConfigOptionStrings>("post_process_plugin");
const bool have_scripts = post_process != nullptr && !post_process->values.empty();
const bool have_plugins = post_process_plugin != nullptr && !post_process_plugin->values.empty();
if (!have_scripts && !have_plugins)
return false;
std::string path;
if (make_copy) {
// Don't run the post-processing script/plugin on the input file, it will be memory mapped by the G-code viewer.
// Make a copy.
path = src_path + ".pp";
// First delete an old file if it exists.
try {
if (boost::filesystem::exists(path))
boost::filesystem::remove(path);
} catch (const std::exception& err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format(
"Failed deleting an old temporary file %1% before running a post-processing script: %2%", path, err.what());
}
// Second make a copy.
std::string error_message;
if (copy_file(src_path, path, error_message, false) != SUCCESS)
throw Slic3r::RuntimeError(
Slic3r::format("Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%", src_path,
error_message));
} else {
// Don't make a copy of the G-code before running the post-processing script.
path = src_path;
}
auto delete_copy = [&path, &src_path, make_copy]() {
if (make_copy)
try {
if (boost::filesystem::exists(path))
boost::filesystem::remove(path);
} catch (const std::exception& err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed deleting a temporary copy %1% of a G-code file %2% : %3%", path,
src_path, err.what());
}
};
auto gcode_file = boost::filesystem::path(path);
if (!boost::filesystem::exists(gcode_file))
throw Slic3r::RuntimeError(std::string("Post-processor can't find exported gcode file"));
// Store print configuration into environment variables.
config.setenv_();
// Let the post-processing script know the target host ("File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet",
// "AstroBox" ...)
boost::nowide::setenv("SLIC3R_PP_HOST", host.c_str(), 1);
// Let the post-processing script know the final file name. For "File" host, it is a full path of the target file name and its location,
// for example pointing to an SD card. For "PrusaLink" or "OctoPrint", it is a file name optionally with a directory on the target host.
boost::nowide::setenv("SLIC3R_PP_OUTPUT_NAME", output_name.c_str(), 1);
// Path to an optional file that the post-processing script may create and populate it with a single line containing the output_name replacement.
std::string path_output_name = path + ".output_name";
auto remove_output_name_file = [&path_output_name, &src_path]() {
try {
if (boost::filesystem::exists(path_output_name))
boost::filesystem::remove(path_output_name);
} catch (const std::exception& err) {
BOOST_LOG_TRIVIAL(error) << Slic3r::format(
"Failed deleting a file %1% carrying the final name / path of a G-code file %2%: %3%", path_output_name, src_path,
err.what());
}
};
// Remove possible stalled path_output_name of the previous run.
remove_output_name_file();
try {
if (have_scripts) {
for (const std::string& scripts : post_process->values) {
std::vector<std::string> lines;
boost::split(lines, scripts, boost::is_any_of("\r\n"));
for (std::string script : lines) {
// Ignore empty post processing script lines.
boost::trim(script);
if (script.empty())
continue;
BOOST_LOG_TRIVIAL(info) << "Executing script " << script << " on file " << path;
std::string std_err;
const int result = run_script(script, gcode_file.string(), std_err);
if (result != 0) {
const std::string msg = std_err.empty() ?
(boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%") %
script % path % result)
.str() :
(boost::format(
"Post-processing script %1% on file %2% failed.\nError code: %3%\nOutput:\n%4%") %
script % path % result % std_err)
.str();
BOOST_LOG_TRIVIAL(error) << msg;
delete_copy();
throw Slic3r::RuntimeError(msg);
}
if (!boost::filesystem::exists(gcode_file)) {
const std::string msg = (boost::format(
_utf8(L("Post-processing script %1% failed.\n\n"
"The post-processing script is expected to change the G-code file %2% in place, but "
"the G-code file was deleted and likely saved under a new name.\n"
"Please adjust the post-processing script to change the G-code in place and consult "
"the manual on how to optionally rename the post-processed G-code file.\n"))) %
script % path)
.str();
BOOST_LOG_TRIVIAL(error) << msg;
throw Slic3r::RuntimeError(msg);
}
}
}
}
if (boost::filesystem::exists(path_output_name)) {
try {
// Read a single line from path_output_name, which should contain the new output name of the post-processed G-code.
boost::nowide::fstream f;
f.open(path_output_name, std::ios::in);
std::string new_output_name;
std::getline(f, new_output_name);
f.close();
if (host == "File") {
namespace fs = boost::filesystem;
fs::path op(new_output_name);
if (op.is_relative() && op.has_filename() && op.parent_path().empty()) {
// Is this just a filename? Make it an absolute path.
auto outpath = fs::path(output_name).parent_path();
outpath /= op.string();
new_output_name = outpath.string();
} else {
if (!op.is_absolute() || !op.has_filename())
throw Slic3r::RuntimeError("Unable to parse desired new path from output name file");
}
if (!fs::exists(fs::path(new_output_name).parent_path()))
throw Slic3r::RuntimeError(
Slic3r::format("Output directory does not exist: %1%", fs::path(new_output_name).parent_path().string()));
}
BOOST_LOG_TRIVIAL(trace) << "Post-processing script changed the file name from " << output_name << " to "
<< new_output_name;
output_name = new_output_name;
} catch (const std::exception& err) {
throw Slic3r::RuntimeError(Slic3r::format("run_post_process_scripts: Failed reading a file %1% "
"carrying the final name / path of a G-code file: %2%",
path_output_name, err.what()));
}
remove_output_name_file();
}
// Run plugins after the scripts so they observe any output_name the scripts produced. A thrown
// exception is handled by the catch below, which removes the temp copy.
if (have_plugins) {
run_post_process_plugins(*post_process_plugin, config.opt<ConfigOptionStrings>("plugins"), path, host, output_name);
}
} catch (...) {
remove_output_name_file();
delete_copy();
throw;
}
src_path = std::move(path);
return true;
}
} // namespace Slic3r
+39
View File
@@ -0,0 +1,39 @@
#ifndef slic3r_GUI_PostProcessor_hpp_
#define slic3r_GUI_PostProcessor_hpp_
#include <functional>
#include <string>
#include "libslic3r/libslic3r.h"
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r {
// Run post-processing scripts (the "post_process" option) and/or post-processing plugins (the
// "post_process_plugin" option) if defined. Lives in the GUI layer because plugins are executed
// through the embedded-Python PluginManager, which libslic3r must not depend on.
// Returns true if a script or plugin was executed.
// Returns false if neither a post-processing script nor plugin was defined.
// Throws an exception on error.
// host is one of "File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ...
// If make_copy, then a temp file will be created for src_path by adding a ".pp" suffix and src_path will be updated.
// In that case the caller is responsible to delete the temp file created. Scripts and plugins always
// run on this working copy so they never touch the original G-code the viewer keeps memory-mapped
// (a writable open of the mapped file fails on Windows with a sharing violation).
// output_name is the final name of the G-code on SD card or when uploaded to PrusaLink or OctoPrint.
// If uploading to PrusaLink or OctoPrint, then the file will be renamed to output_name first on the target host.
// The post-processing script may change the output_name.
extern bool run_post_process_scripts(
std::string& src_path, bool make_copy, const std::string& host, std::string& output_name, const DynamicPrintConfig& config);
inline bool run_post_process_scripts(std::string& src_path, const DynamicPrintConfig& config)
{
std::string src_path_name = src_path;
return run_post_process_scripts(src_path, false, "File", src_path_name, config);
}
// BBS
extern void gcode_add_line_number(const std::string& path, const DynamicPrintConfig& config);
} // namespace Slic3r
#endif /* slic3r_GUI_PostProcessor_hpp_ */
+254
View File
@@ -0,0 +1,254 @@
#include "ProcessRunner.hpp"
#include <boost/process/env.hpp>
#include <boost/process.hpp>
#ifdef _WIN32
#include <boost/process/windows.hpp>
#endif
namespace Slic3r { namespace GUI {
ProcessRunner::ProcessRunner()
: m_poll_timer(this, wxID_ANY)
{
Bind(wxEVT_TIMER, &ProcessRunner::on_timer, this);
}
ProcessRunner::~ProcessRunner()
{
terminate();
}
void ProcessRunner::join_launch_thread()
{
if (m_launch_thread.joinable())
m_launch_thread.join();
}
void ProcessRunner::start_reader_threads()
{
join_reader_threads();
if (m_stdout_pipe) {
m_stdout_thread = std::thread([this]() {
std::string line;
while (std::getline(*m_stdout_pipe, line))
enqueue_output(std::move(line), false);
});
}
if (m_stderr_pipe) {
m_stderr_thread = std::thread([this]() {
std::string line;
while (std::getline(*m_stderr_pipe, line))
enqueue_output(std::move(line), true);
});
}
}
void ProcessRunner::join_reader_threads()
{
if (m_stdout_thread.joinable())
m_stdout_thread.join();
if (m_stderr_thread.joinable())
m_stderr_thread.join();
}
void ProcessRunner::enqueue_output(std::string line, bool is_stderr)
{
std::lock_guard<std::mutex> lock(m_output_mutex);
m_output_queue.push_back({std::move(line), is_stderr});
}
std::vector<ProcessRunner::OutputLine> ProcessRunner::drain_output_queue()
{
std::lock_guard<std::mutex> lock(m_output_mutex);
std::vector<OutputLine> batch;
batch.swap(m_output_queue);
return batch;
}
// Clean up stored process state after the child has exited or has already been stopped.
void ProcessRunner::finish_process(int exit_code)
{
join_launch_thread();
join_reader_threads();
std::vector<OutputLine> final_batch = drain_output_queue();
if (!final_batch.empty() && m_on_output)
m_on_output(final_batch);
m_process.reset();
m_stdout_pipe.reset();
m_stderr_pipe.reset();
m_stdin_pipe.reset();
if (m_on_done) {
auto cb = std::move(m_on_done);
m_on_done = nullptr;
cb(exit_code);
}
}
// Stop a running child process, then use finish_process() for shared cleanup.
void ProcessRunner::terminate()
{
m_poll_timer.Stop();
join_launch_thread();
if (m_process && m_process->running()) {
std::error_code ec;
m_process->terminate(ec);
m_process->wait(ec);
}
if (m_process) {
finish_process(m_process->exit_code());
} else if (m_launch_failed.exchange(false) || m_on_done) {
finish_process(-1);
}
}
bool ProcessRunner::run_command_line_async(const std::string& command_line,
OutputCallback on_output,
DoneCallback on_done)
{
if (!m_launching.load())
join_launch_thread();
if (is_running())
return false;
m_on_output = std::move(on_output);
m_on_done = std::move(on_done);
drain_output_queue();
m_launch_failed.store(false);
m_launching.store(true);
m_launch_thread = std::thread([this, command_line]() {
namespace bp = boost::process;
try {
auto stdout_pipe = std::make_unique<bp::ipstream>();
auto stderr_pipe = std::make_unique<bp::ipstream>();
auto stdin_pipe = std::make_unique<bp::opstream>();
auto process = std::make_unique<bp::child>(
bp::cmd = command_line,
bp::env["PYTHONUNBUFFERED"] = "1",
bp::env["PYTHONIOENCODING"] = "utf-8",
bp::env["PYTHONUTF8"] = "1",
#ifdef _WIN32
bp::windows::create_no_window,
#endif
bp::std_in < *stdin_pipe,
bp::std_out > *stdout_pipe,
bp::std_err > *stderr_pipe);
m_stdout_pipe = std::move(stdout_pipe);
m_stderr_pipe = std::move(stderr_pipe);
m_stdin_pipe = std::move(stdin_pipe);
m_process = std::move(process);
start_reader_threads();
} catch (const std::exception&) {
m_stdout_pipe.reset();
m_stderr_pipe.reset();
m_stdin_pipe.reset();
m_process.reset();
m_launch_failed.store(true);
}
m_launching.store(false);
});
m_poll_timer.Start(50);
return true;
}
void ProcessRunner::on_timer(wxTimerEvent& /*event*/)
{
if (m_launching.load())
return;
join_launch_thread();
if (m_launch_failed.exchange(false)) {
m_poll_timer.Stop();
finish_process(-1);
return;
}
if (!m_process) {
m_poll_timer.Stop();
return;
}
std::vector<OutputLine> batch = drain_output_queue();
if (!batch.empty() && m_on_output)
m_on_output(batch);
// Check if process exited
if (!m_process->running()) {
m_poll_timer.Stop();
m_process->wait();
const int exit_code = m_process->exit_code();
finish_process(exit_code);
}
}
void ProcessRunner::write_stdin(const std::string& data)
{
if (m_launching.load() || !m_stdin_pipe || !m_process || !m_process->running())
return;
*m_stdin_pipe << data;
m_stdin_pipe->flush();
}
bool ProcessRunner::is_running() const
{
return m_launching.load() || (m_process && m_process->running());
}
ProcessRunner::SyncResult ProcessRunner::run_sync(const std::string& executable,
const std::vector<std::string>& args)
{
SyncResult result;
try {
namespace bp = boost::process;
bp::ipstream std_out;
bp::ipstream std_err;
bp::child child(executable, bp::args(args),
bp::std_out > std_out,
bp::std_err > std_err);
// Read both streams on separate threads while the process runs
std::thread stdout_reader([&]() {
std::string line;
while (std::getline(std_out, line))
result.stdout_output += line + '\n';
});
std::thread stderr_reader([&]() {
std::string line;
while (std::getline(std_err, line))
result.stderr_output += line + '\n';
});
child.wait();
stdout_reader.join();
stderr_reader.join();
result.exit_code = child.exit_code();
} catch (const std::exception&) {
result.exit_code = -1;
}
return result;
}
}} // namespace Slic3r::GUI
+93
View File
@@ -0,0 +1,93 @@
#ifndef slic3r_GUI_ProcessRunner_hpp_
#define slic3r_GUI_ProcessRunner_hpp_
#include <wx/event.h>
#include <wx/timer.h>
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <functional>
#include <boost/process.hpp>
namespace Slic3r { namespace GUI {
class ProcessRunner : public wxEvtHandler
{
public:
ProcessRunner();
~ProcessRunner() override;
struct OutputLine {
std::string text;
bool is_stderr;
};
using OutputCallback = std::function<void(const std::vector<OutputLine>& lines)>;
using DoneCallback = std::function<void(int exit_code)>;
// Non-blocking command-line style launch. Boost parses command_line into
// executable + arguments.
// Output is batched per timer tick (50ms) and delivered via on_output on the GUI thread.
// on_done fires on the GUI thread with the exit code.
// Returns false if a process is already running.
bool run_command_line_async(const std::string& command_line,
OutputCallback on_output,
DoneCallback on_done);
struct SyncResult {
int exit_code = -1;
std::string stdout_output;
std::string stderr_output;
};
// Blocking: runs the process and waits for it to finish.
// Safe to call from any thread (no wx dependency).
static SyncResult run_sync(const std::string& executable,
const std::vector<std::string>& args);
// Write data to the running process's stdin (async mode only).
void write_stdin(const std::string& data);
// True while an async process is launching or running.
bool is_running() const;
// Terminate the running process (async mode only).
void terminate();
private:
void on_timer(wxTimerEvent& event);
void finish_process(int exit_code);
void start_reader_threads();
void join_reader_threads();
void enqueue_output(std::string line, bool is_stderr);
std::vector<OutputLine> drain_output_queue();
// Join any completed background launcher thread before reusing or destroying it.
void join_launch_thread();
wxTimer m_poll_timer;
// True while the launcher thread is still creating the child process; join only after this becomes false.
std::atomic_bool m_launching{false};
std::atomic_bool m_launch_failed{false};
std::thread m_launch_thread;
std::thread m_stdout_thread;
std::thread m_stderr_thread;
std::mutex m_output_mutex;
std::vector<OutputLine> m_output_queue;
std::unique_ptr<boost::process::child> m_process;
std::unique_ptr<boost::process::ipstream> m_stdout_pipe;
std::unique_ptr<boost::process::ipstream> m_stderr_pipe;
std::unique_ptr<boost::process::opstream> m_stdin_pipe;
OutputCallback m_on_output;
DoneCallback m_on_done;
};
}} // namespace Slic3r::GUI
#endif
+37 -18
View File
@@ -1075,6 +1075,28 @@ void add_correct_opts_to_options_list(const std::string &opt_key, std::map<std::
map.emplace(opt_key + "#0", value);
}
std::string Tab::options_list_storage_key(const std::string& opt_key) const
{
if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" ||
opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area")
return opt_key;
if (m_config == nullptr || !m_config->has(opt_key))
return opt_key;
const ConfigOption* option = m_config->option(opt_key);
if (option == nullptr || !option->is_vector())
return opt_key;
const ConfigOptionDef* def = m_config->def()->get(opt_key);
if (def == nullptr)
return opt_key;
const bool serialized = def->gui_flags == "serialized";
const bool is_plugin_field = def->gui_type == ConfigOptionDef::GUIType::plugin_picker;
return (serialized || is_plugin_field) ? opt_key : opt_key + "#0";
}
void Tab::update_all_extruder_options_status()
{
if (!m_extruder_switch && !m_variant_combo) {
@@ -1243,23 +1265,14 @@ void Tab::check_extruder_options_status(int index, bool &sys_extruder, bool &mod
}
}
}
void Tab::init_options_list()
{
if (!m_options_list.empty())
m_options_list.clear();
for (const std::string& opt_key : m_config->keys())
{
if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area") {
m_options_list.emplace(opt_key, m_opt_status_value);
continue;
}
const ConfigOptionDef* opt_def = m_config->def()->get(opt_key);
if (m_config->option(opt_key)->is_vector() && !(opt_def && opt_def->gui_flags == "serialized"))
m_options_list.emplace(opt_key + "#0", m_opt_status_value);
else
m_options_list.emplace(opt_key, m_opt_status_value);
}
m_options_list.emplace(options_list_storage_key(opt_key), m_opt_status_value);
}
void TabPrinter::init_options_list()
@@ -2047,7 +2060,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
bool is_safe_to_rotate = _sparse_infill_pattern == ipRectilinear || _sparse_infill_pattern == ipLine ||
_sparse_infill_pattern == ipZigZag || _sparse_infill_pattern == ipCrossZag ||
_sparse_infill_pattern == ipLockedZag;
auto new_value = boost::any_cast<std::string>(value);
is_safe_to_rotate = is_safe_to_rotate || new_value.empty();
const bool had_previous_value = !m_last_sparse_infill_rotate_template_value.empty();
@@ -3060,6 +3073,12 @@ void TabPrint::build()
option.opt.height = 15;
optgroup->append_single_option_line(option, "others_settings_post_processing_scripts");
optgroup = page->new_optgroup(L("Post-processing Plugin"), L"param_gcode", 0);
optgroup->hide_labels();
option = optgroup->get_option("post_process_plugin");
option.opt.full_width = true;
optgroup->append_single_option_line(option, "others_settings_plugin_picker");
optgroup = page->new_optgroup(L("Notes"), "note", 0);
option = optgroup->get_option("notes");
option.opt.full_width = true;
@@ -4137,7 +4156,7 @@ void TabFilament::update_filament_overrides_page(const DynamicPrintConfig* print
if (og_ironing_it != page->m_optgroups.end())
{
ConfigOptionsGroupShp ironing_optgroup = *og_ironing_it;
std::vector<std::string> ironing_opt_keys = {
"filament_ironing_flow",
"filament_ironing_spacing",
@@ -4149,7 +4168,7 @@ void TabFilament::update_filament_overrides_page(const DynamicPrintConfig* print
{
if (m_overrides_options.find(opt_key) == m_overrides_options.end())
continue;
bool is_checked = !dynamic_cast<ConfigOptionVectorBase*>(m_config->option(opt_key))->is_nil(extruder_idx);
m_overrides_options[opt_key]->Enable(true);
m_overrides_options[opt_key]->SetValue(is_checked);
@@ -4463,7 +4482,7 @@ void TabFilament::build()
optgroup->append_single_option_line("filament_tower_ironing_area", "material_multimaterial#multimaterial-wipe-tower-parameters");
optgroup->append_single_option_line("filament_tower_interface_purge_volume", "material_multimaterial#multimaterial-wipe-tower-parameters");
optgroup->append_single_option_line("filament_tower_interface_print_temp", "material_multimaterial#multimaterial-wipe-tower-parameters");
optgroup = page->new_optgroup(L("Multi Filament"));
// optgroup->append_single_option_line("filament_flush_temp", "", 0);
// optgroup->append_single_option_line("filament_flush_volumetric_speed", "", 0);
@@ -5827,7 +5846,7 @@ void TabPrinter::toggle_options()
const bool support_parallel_printheads = printer_cfg.opt_bool("support_parallel_printheads");
toggle_line("parallel_printheads_count", support_parallel_printheads);
}
if (m_active_page->title() == L("Machine G-code")) {
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
@@ -7814,7 +7833,7 @@ std::vector<wxString> Tab::generate_extruder_options()
wxString extruder_name = _L(DevPrinterConfigUtil::get_toolhead_display_name(
pt, ext_id, ToolHeadComponent::Nozzle, ToolHeadNameCase::TitleCase, true));
NozzleVolumeType volume_type = NozzleVolumeType(nozzle_volumes->values[i]);
// TODO: Orca: Support hybrid
/*if (volume_type == NozzleVolumeType::nvtHybrid) {
options.push_back(wxString::Format(_L("%s: %s"), extruder_name, _L("Standard")));
@@ -7868,7 +7887,7 @@ bool Tab::get_extruder_sync_enable_state(int extruder_id)
if (left_nozzle == right_nozzle) {
return true;
}
// TODO: Orca: Support hybrid
//if (left_nozzle != NozzleVolumeType::nvtHybrid && right_nozzle != NozzleVolumeType::nvtHybrid) {
// return false;
+1
View File
@@ -383,6 +383,7 @@ public:
virtual void update() = 0;
virtual void toggle_options() = 0;
virtual void init_options_list();
std::string options_list_storage_key(const std::string& opt_key) const;
virtual void update_custom_dirty(std::vector<std::string> &dirty_options, std::vector<std::string> &nonsys_options) {}
void load_initial_data();
void update_dirty();
+163
View File
@@ -0,0 +1,163 @@
#include "TerminalDialog.hpp"
#include "ProcessRunner.hpp"
#include "I18N.hpp"
#include "slic3r/plugin/PythonInterpreter.hpp"
#include <wx/sizer.h>
#include <cctype>
namespace Slic3r { namespace GUI {
namespace {
bool consume_command_prefix(const std::string& command, const std::string& prefix, std::string& remainder)
{
size_t pos = 0;
while (pos < command.size() && std::isspace(static_cast<unsigned char>(command[pos])))
++pos;
if (command.compare(pos, prefix.size(), prefix) != 0)
return false;
pos += prefix.size();
if (pos < command.size() && !std::isspace(static_cast<unsigned char>(command[pos])))
return false;
while (pos < command.size() && std::isspace(static_cast<unsigned char>(command[pos])))
++pos;
remainder = command.substr(pos);
return true;
}
std::string quote_command_line_arg(const std::string& value)
{
std::string quoted = "\"";
for (const char ch : value) {
if (ch == '"')
quoted += "\\\"";
else
quoted += ch;
}
quoted += "\"";
return quoted;
}
} // namespace
TerminalDialog::TerminalDialog(wxWindow* parent, wxWindowID id, const wxString& title,
const wxPoint& pos, const wxSize& size, long style)
: WebViewHostDialog(parent, id, title, pos, size, style)
{
m_runner = std::make_unique<ProcessRunner>();
create_webview("web/dialog/TerminalDialog/index.html", _L("Terminal"),
wxSize(820, 600), wxSize(640, 480));
}
TerminalDialog::~TerminalDialog()
{
if (m_runner && m_runner->is_running())
m_runner->terminate();
}
void TerminalDialog::on_script_message(const nlohmann::json& payload)
{
const std::string command = payload.value("command", "");
if (command == "run_command") {
std::string cmd = payload.value("cmd", "");
if (!cmd.empty())
resolve_and_run(cmd);
}
else if (command == "write_stdin") {
std::string data = payload.value("data", "");
if (!data.empty() && m_runner && m_runner->is_running())
m_runner->write_stdin(data);
}
else {
handle_common_script_command(payload);
}
}
void TerminalDialog::resolve_and_run(const std::string& cmd)
{
if (m_runner->is_running()) {
nlohmann::json err;
err["command"] = "process_error";
err["message"] = "A command is already running.";
call_web_handler(err);
return;
}
std::string executable;
std::string arg_string;
auto send_process_error = [this](const std::string& message) {
nlohmann::json err;
err["command"] = "process_error";
err["message"] = message;
call_web_handler(err);
};
// Parse command: must start with "python" or "uv"
if (consume_command_prefix(cmd, "python", arg_string)) {
const std::string python_path = PythonInterpreter::bundled_python_executable();
if (python_path.empty()) {
send_process_error("Bundled Python executable not found.");
return;
}
executable = python_path;
}
else if (consume_command_prefix(cmd, "uv", arg_string)) {
const std::string uv_path = PythonInterpreter::bundled_uv_path();
if (uv_path.empty()) {
send_process_error("uv executable not found.");
return;
}
executable = uv_path;
}
else {
send_process_error("Only 'python' and 'uv' commands are supported.");
return;
}
std::string command_line = quote_command_line_arg(executable);
if (!arg_string.empty()) {
command_line += " ";
command_line += arg_string;
}
bool started = m_runner->run_command_line_async(
command_line,
[this](const std::vector<ProcessRunner::OutputLine>& lines) {
nlohmann::json msg;
msg["command"] = "output";
nlohmann::json lines_arr = nlohmann::json::array();
for (const auto& line : lines) {
nlohmann::json l;
l["text"] = line.text;
l["is_stderr"] = line.is_stderr;
lines_arr.push_back(std::move(l));
}
msg["lines"] = std::move(lines_arr);
call_web_handler(msg);
},
[this](int exit_code) {
nlohmann::json msg;
msg["command"] = "process_done";
msg["exit_code"] = exit_code;
call_web_handler(msg);
});
if (!started) {
nlohmann::json err;
err["command"] = "process_error";
err["message"] = "Failed to start process.";
call_web_handler(err);
}
}
}} // namespace Slic3r::GUI
+36
View File
@@ -0,0 +1,36 @@
#ifndef slic3r_TerminalDialog_hpp_
#define slic3r_TerminalDialog_hpp_
#include "Widgets/WebViewHostDialog.hpp"
#include <memory>
#include <string>
#include <vector>
namespace Slic3r { namespace GUI {
class ProcessRunner;
class TerminalDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
TerminalDialog(wxWindow* parent,
wxWindowID id = wxID_ANY,
const wxString& title = wxT(""),
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX);
~TerminalDialog() override;
private:
void on_script_message(const nlohmann::json& payload) override;
void resolve_and_run(const std::string& cmd);
std::unique_ptr<ProcessRunner> m_runner;
};
}} // namespace Slic3r::GUI
#endif
@@ -0,0 +1,133 @@
#include "WebViewHostDialog.hpp"
#include "WebView.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <wx/log.h>
#include <wx/sizer.h>
namespace Slic3r { namespace GUI {
WebViewHostDialog::WebViewHostDialog(wxWindow* parent,
wxWindowID id,
const wxString& title,
const wxPoint& pos,
const wxSize& size,
long style)
: DPIDialog(parent, id, title, pos, size, style)
{
SetBackgroundColour(*wxWHITE);
}
bool WebViewHostDialog::create_webview(const std::string& resource_path,
const wxString& title,
const wxSize& dialog_size,
const wxSize& min_size)
{
SetTitle(title);
SetMinSize(FromDIP(min_size));
const wxString target_url = build_resource_url(resource_path);
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
m_browser = WebView::CreateWebView(this, target_url);
if (m_browser == nullptr) {
wxLogError("Could not init m_browser");
delete topsizer;
return false;
}
SetSizer(topsizer);
topsizer->Add(m_browser, wxSizerFlags().Expand().Proportion(1));
SetSize(FromDIP(dialog_size));
CenterOnParent();
Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, &WebViewHostDialog::on_script_message_event, this, m_browser->GetId());
load_url(target_url);
wxGetApp().UpdateDlgDarkUI(this);
return true;
}
wxString WebViewHostDialog::build_resource_url(const std::string& resource_path) const
{
wxString target_url = from_u8((boost::filesystem::path(resources_dir()) / resource_path).make_preferred().string());
if (append_language_to_url()) {
const wxString lang = wxGetApp().current_language_code_safe();
if (!lang.empty()) {
target_url += wxT("?lang=");
target_url += lang;
}
}
return wxString("file://") + target_url;
}
void WebViewHostDialog::load_url(const wxString& url)
{
if (!m_browser)
return;
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << " enter, url=" << into_u8(url);
WebView::LoadUrl(m_browser, url);
m_browser->SetFocus();
}
bool WebViewHostDialog::run_script(const wxString& script)
{
if (!m_browser)
return false;
return WebView::RunScript(m_browser, script);
}
void WebViewHostDialog::call_web_handler(const nlohmann::json& payload, const wxString& handler)
{
const wxString payload_text = wxString::FromUTF8(payload.dump(-1, ' ', false, nlohmann::json::error_handler_t::ignore));
const wxString script = handler + wxT("(") + payload_text + wxT(")");
wxGetApp().CallAfter([this, script] { run_script(script); });
}
bool WebViewHostDialog::handle_common_script_command(const nlohmann::json& payload, int close_return_code)
{
const std::string command = payload.value("command", "");
if (command == "close_page") {
if (IsModal())
EndModal(close_return_code);
else
Close();
return true;
}
return false;
}
void WebViewHostDialog::on_dpi_changed(const wxRect&)
{
Refresh();
}
void WebViewHostDialog::on_script_message_parse_error(const wxString& payload, const std::exception& error)
{
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << "; payload=" << into_u8(payload) << "; error=" << error.what();
}
void WebViewHostDialog::on_script_message_event(wxWebViewEvent& event)
{
const wxString payload = event.GetString();
try {
on_script_message(nlohmann::json::parse(payload.utf8_string()));
} catch (const std::exception& e) {
on_script_message_parse_error(payload, e);
}
}
}} // namespace Slic3r::GUI
@@ -0,0 +1,56 @@
#ifndef slic3r_GUI_Widgets_WebViewHostDialog_hpp_
#define slic3r_GUI_Widgets_WebViewHostDialog_hpp_
#include <nlohmann/json.hpp>
#include <slic3r/GUI/GUI_Utils.hpp>
#include <exception>
#include <string>
#include <wx/string.h>
#include <wx/webview.h>
namespace Slic3r { namespace GUI {
// Shared shell for local HTML dialogs that communicate through window.wx.postMessage().
class WebViewHostDialog : public Slic3r::GUI::DPIDialog
{
public:
WebViewHostDialog(wxWindow* parent,
wxWindowID id = wxID_ANY,
const wxString& title = wxT(""),
const wxPoint& pos = wxDefaultPosition,
const wxSize& size = wxDefaultSize,
long style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX);
~WebViewHostDialog() override = default;
bool create_webview(const std::string& resource_path,
const wxString& title,
const wxSize& dialog_size = wxSize(820, 660),
const wxSize& min_size = wxSize(640, 640));
void load_url(const wxString& url);
bool run_script(const wxString& script);
void call_web_handler(const nlohmann::json& payload, const wxString& handler = wxT("HandleStudio"));
protected:
wxWebView* browser() const { return m_browser; }
wxString build_resource_url(const std::string& resource_path) const;
bool handle_common_script_command(const nlohmann::json& payload, int close_return_code = wxID_CANCEL);
void on_dpi_changed(const wxRect& suggested_rect) override;
virtual void on_script_message(const nlohmann::json& payload) = 0;
virtual void on_script_message_parse_error(const wxString& payload, const std::exception& error);
virtual bool append_language_to_url() const { return true; }
private:
void on_script_message_event(wxWebViewEvent& event);
wxWebView* m_browser{nullptr};
};
}} // namespace Slic3r::GUI
#endif
+2 -1
View File
@@ -2,4 +2,5 @@
#define ORCA_UPDATER_SIG_KEY_B64 "@ORCA_UPDATER_SIG_KEY_B64@"
#define ORCA_UPDATER_SIG_KEY_AVAILABLE @ORCA_UPDATER_SIG_KEY_AVAILABLE@
#define ORCA_BUNDLED_PYTHON_ROOT "@_bundled_python_root@"
#define ORCA_BUNDLED_UV_EXECUTABLE "@ORCA_BUNDLED_UV_EXECUTABLE_CONFIG@"
+2
View File
@@ -48,6 +48,8 @@ enum class FilamentSyncMode {
*
* Implementations:
* - OrcaPrinterAgent: Stub implementation (printer ops not yet supported)
* - PrinterAgentPluginCapability: Python printer-agent plugin capability that
* implements IPrinterAgent directly and is handed out as the live agent
* - BBLPrinterAgent: Wrapper around Bambu Lab's proprietary DLL
*
* Token Access:
+4 -5
View File
@@ -115,15 +115,11 @@ void NetworkAgent::add_cloud_agent(const std::string& provider, std::shared_ptr<
void NetworkAgent::set_printer_agent(std::shared_ptr<IPrinterAgent> printer_agent)
{
if (!printer_agent) {
return;
}
// Disconnect all callbacks from the old agent
auto old_printer_agent = m_printer_agent;
m_printer_agent = std::move(printer_agent);
m_printer_agent_id = m_printer_agent->get_agent_info().id;
m_printer_agent_id = m_printer_agent ? m_printer_agent->get_agent_info().id : "";
// Disconnect the old agent's connections/threads.
if (old_printer_agent && old_printer_agent != m_printer_agent) {
@@ -131,6 +127,9 @@ void NetworkAgent::set_printer_agent(std::shared_ptr<IPrinterAgent> printer_agen
apply_printer_callbacks(old_printer_agent, {});
}
if (!m_printer_agent)
return;
apply_printer_callbacks(m_printer_agent, m_printer_callbacks);
}
+309 -25
View File
@@ -6,16 +6,29 @@
#include "QidiPrinterAgent.hpp"
#include "SnapmakerPrinterAgent.hpp"
#include "MoonrakerPrinterAgent.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
#include "CrealityPrintAgent.hpp"
#include <algorithm>
#include <boost/log/trivial.hpp>
#include <chrono>
#include <map>
#include <mutex>
#include <utility>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
namespace Slic3r {
namespace {
static std::mutex s_registry_mutex;
struct PythonPrinterAgentRegistrationToken {};
static std::map<std::pair<std::string, std::string>, std::shared_ptr<PythonPrinterAgentRegistrationToken>>
s_python_printer_agent_registration_tokens;
std::map<std::string, PrinterAgentInfo>& get_printer_agents()
{
static std::map<std::string, PrinterAgentInfo> agents;
@@ -28,22 +41,32 @@ std::map<std::string, std::shared_ptr<IPrinterAgent>>& get_printer_agent_cache()
return cache;
}
std::map<std::pair<std::string, std::string>, std::string>& get_python_printer_agent_ids()
{
static std::map<std::pair<std::string, std::string>, std::string> capability_to_agent_id;
return capability_to_agent_id;
}
std::string plugin_printer_agent_full_ref(const PluginDescriptor& descriptor, const std::string& capability_name)
{
const std::string identity = descriptor.is_cloud_plugin() ? descriptor.name : descriptor.plugin_key;
return identity + ';' + descriptor.cloud_uuid() + ';' + capability_name;
}
// Helper to register a printer agent type with the standard factory pattern.
// AgentTypes that take a log_dir constructor arg use the default; BBLPrinterAgent
// (no log_dir) is registered separately.
template<typename T>
void register_agent()
template<typename T> void register_agent()
{
auto info = T::get_agent_info_static();
NetworkAgentFactory::register_printer_agent(
info.id, info.name,
[](std::shared_ptr<ICloudServiceAgent> cloud_agent,
const std::string& log_dir) -> std::shared_ptr<IPrinterAgent> {
auto agent = std::make_shared<T>(log_dir);
if (cloud_agent)
agent->set_cloud_agent(cloud_agent);
return agent;
});
NetworkAgentFactory::register_printer_agent(info.id, info.name,
[](std::shared_ptr<ICloudServiceAgent> cloud_agent,
const std::string& log_dir) -> std::shared_ptr<IPrinterAgent> {
auto agent = std::make_shared<T>(log_dir);
if (cloud_agent)
agent->set_cloud_agent(cloud_agent);
return agent;
});
}
} // anonymous namespace
@@ -51,48 +74,62 @@ void register_agent()
bool NetworkAgentFactory::register_printer_agent(const std::string& id, const std::string& display_name, PrinterAgentFactory factory)
{
std::lock_guard<std::mutex> lock(s_registry_mutex);
auto& agents = get_printer_agents();
auto& agents = get_printer_agents();
return agents.emplace(id, PrinterAgentInfo(id, display_name, std::move(factory))).second;
}
bool NetworkAgentFactory::is_printer_agent_registered(const std::string& id)
{
std::lock_guard<std::mutex> lock(s_registry_mutex);
auto& agents = get_printer_agents();
auto& agents = get_printer_agents();
return agents.find(id) != agents.end();
}
const PrinterAgentInfo* NetworkAgentFactory::get_printer_agent_info(const std::string& id)
{
std::lock_guard<std::mutex> lock(s_registry_mutex);
auto& agents = get_printer_agents();
auto it = agents.find(id);
auto& agents = get_printer_agents();
auto it = agents.find(id);
return (it != agents.end()) ? &it->second : nullptr;
}
std::string NetworkAgentFactory::get_printer_agent_plugin_identifier(const std::string& id)
{
std::lock_guard<std::mutex> lock(s_registry_mutex);
auto& agents = get_printer_agents();
auto it = agents.find(id);
return (it != agents.end() && it->second.is_plugin()) ? it->second.plugin_identifier : std::string();
}
std::vector<PrinterAgentInfo> NetworkAgentFactory::get_registered_printer_agents()
{
std::lock_guard<std::mutex> lock(s_registry_mutex);
auto& agents = get_printer_agents();
std::lock_guard<std::mutex> lock(s_registry_mutex);
auto& agents = get_printer_agents();
std::vector<PrinterAgentInfo> result;
result.reserve(agents.size());
for (const auto& pair : agents) {
result.push_back(pair.second);
if (!pair.second.is_plugin())
result.push_back(pair.second);
}
for (const auto& pair : agents) {
if (pair.second.is_plugin())
result.push_back(pair.second);
}
return result;
}
std::shared_ptr<IPrinterAgent> NetworkAgentFactory::create_printer_agent_by_id(const std::string& id,
std::shared_ptr<IPrinterAgent> NetworkAgentFactory::create_printer_agent_by_id(const std::string& id,
std::shared_ptr<ICloudServiceAgent> cloud_agent,
const std::string& log_dir)
const std::string& log_dir)
{
std::lock_guard<std::mutex> lock(s_registry_mutex);
// Check cache first
auto& cache = get_printer_agent_cache();
auto cache_it = cache.find(id);
auto& cache = get_printer_agent_cache();
auto cache_it = cache.find(id);
if (cache_it != cache.end()) {
BOOST_LOG_TRIVIAL(info) << "Reusing cached printer agent: " << id;
if (cloud_agent)
@@ -102,7 +139,7 @@ std::shared_ptr<IPrinterAgent> NetworkAgentFactory::create_printer_agent_by_id(c
// Not cached — create via factory
auto& agents = get_printer_agents();
auto it = agents.find(id);
auto it = agents.find(id);
if (it == agents.end()) {
BOOST_LOG_TRIVIAL(warning) << "Unknown printer agent ID: " << id;
@@ -120,7 +157,7 @@ std::shared_ptr<IPrinterAgent> NetworkAgentFactory::create_printer_agent_by_id(c
void NetworkAgentFactory::clear_printer_agent_cache()
{
std::lock_guard<std::mutex> lock(s_registry_mutex);
auto& cache = get_printer_agent_cache();
auto& cache = get_printer_agent_cache();
for (auto& pair : cache) {
if (pair.second)
pair.second->disconnect_printer();
@@ -139,7 +176,8 @@ void NetworkAgentFactory::register_all_agents()
// for K-series boards with CFS support.
register_agent<MoonrakerPrinterAgent>();
// BBLPrinterAgent takes no constructor args, so register manually
// Keep BBL as a built-in option. Python printer-agent plugins with the
// same AgentInfo ID are listed separately under the plugin registry key.
{
auto info = BBLPrinterAgent::get_agent_info_static();
register_printer_agent(info.id, info.name,
@@ -189,4 +227,250 @@ std::unique_ptr<NetworkAgent> create_agent_from_config(const std::string& log_di
return agent;
}
void NetworkAgentFactory::register_python_plugin(const std::string& plugin_key)
{
auto capabilities = PluginManager::instance().get_loader().get_plugin_capabilities_by_type(
plugin_key, PluginCapabilityType::PrinterConnection);
for (const auto& capability : capabilities)
if (capability && capability->enabled)
register_python_printer_agent(plugin_key, capability->name);
}
void NetworkAgentFactory::register_python_printer_agent(const std::string& plugin_key, const std::string& capability_name)
{
PluginManager& plugin_manager = PluginManager::instance();
auto cap = plugin_manager.get_loader().get_plugin_capability_by_name(plugin_key, PluginCapabilityType::PrinterConnection,
capability_name);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << "Printer-agent capability '" << capability_name << "' not found for plugin '" << plugin_key << "'";
return;
}
if (!cap->enabled) {
BOOST_LOG_TRIVIAL(warning) << "Printer-agent capability '" << capability_name << "' is disabled for plugin '" << plugin_key << "'";
return;
}
PluginDescriptor descriptor;
if (!plugin_manager.get_catalog().try_get_plugin_descriptor(plugin_key, descriptor)) {
BOOST_LOG_TRIVIAL(warning) << "Could not find descriptor for plugin key: '" << plugin_key;
return;
}
auto plugin = std::dynamic_pointer_cast<PrinterAgentPluginCapability>(cap->instance);
if (!plugin) {
BOOST_LOG_TRIVIAL(warning) << "Loaded plugin capability '" << capability_name << "' is not a printer-agent plugin";
return;
}
auto info = plugin->get_agent_info();
if (info.id.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' returned an empty agent ID";
return;
}
const auto capability_key = std::make_pair(plugin_key, capability_name);
std::shared_ptr<PythonPrinterAgentRegistrationToken> registration_token;
{
std::lock_guard<std::mutex> lock(s_registry_mutex);
auto token_it = std::find_if(s_python_printer_agent_registration_tokens.begin(), s_python_printer_agent_registration_tokens.end(),
[&plugin_key, &capability_name](const auto& entry) {
return entry.first.first == plugin_key && entry.first.second == capability_name;
});
if (token_it == s_python_printer_agent_registration_tokens.end()) {
registration_token = std::make_shared<PythonPrinterAgentRegistrationToken>();
s_python_printer_agent_registration_tokens.emplace(capability_key, registration_token);
} else {
registration_token = token_it->second;
}
}
const std::string plugin_full_ref = plugin_printer_agent_full_ref(descriptor, capability_name);
std::weak_ptr<PrinterAgentPluginCapability> weak_plugin = plugin;
PrinterAgentFactory factory = [weak_plugin](std::shared_ptr<ICloudServiceAgent> cloud_agent,
const std::string& /*log_dir*/) -> std::shared_ptr<IPrinterAgent> {
// The capability implements IPrinterAgent directly, so it is handed out as the
// live agent (no adapter); the Python plugin services the calls.
auto plugin = weak_plugin.lock();
if (!plugin)
return nullptr;
if (cloud_agent)
plugin->set_cloud_agent(cloud_agent);
return plugin;
};
// Python work above may allow disable/unload/reload to replace this capability.
// Re-resolve without holding the registry mutex to avoid lock inversion and reentrancy.
auto current_cap = plugin_manager.get_loader().get_plugin_capability_by_name(plugin_key, PluginCapabilityType::PrinterConnection,
capability_name);
if (current_cap != cap || !current_cap->enabled) {
BOOST_LOG_TRIVIAL(debug) << "Printer-agent capability '" << capability_name << "' from plugin '" << plugin_key
<< "' changed or was disabled during registration";
return;
}
std::shared_ptr<IPrinterAgent> cached_agent;
{
std::lock_guard<std::mutex> lock(s_registry_mutex);
auto token_it = std::find_if(s_python_printer_agent_registration_tokens.begin(), s_python_printer_agent_registration_tokens.end(),
[&plugin_key, &capability_name](const auto& entry) {
return entry.first.first == plugin_key && entry.first.second == capability_name;
});
if (token_it == s_python_printer_agent_registration_tokens.end() || token_it->second != registration_token || !cap->enabled) {
BOOST_LOG_TRIVIAL(debug) << "Printer-agent capability '" << capability_name << "' from plugin '" << plugin_key
<< "' was disabled before registry commit";
return;
}
auto& python_agent_ids = get_python_printer_agent_ids();
for (const auto& pair : python_agent_ids) {
if (pair.first != capability_key && pair.second == info.id) {
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses duplicate agent ID '" << info.id
<< "' already registered by capability '" << pair.first.second << "' from plugin '"
<< pair.first.first << "'";
return;
}
}
auto plugin_it = python_agent_ids.find(capability_key);
if (plugin_it != python_agent_ids.end() && plugin_it->second != info.id) {
const std::string old_agent_id = plugin_it->second;
get_printer_agents().erase(old_agent_id);
auto& cache = get_printer_agent_cache();
auto cache_it = cache.find(old_agent_id);
if (cache_it != cache.end()) {
cached_agent = std::move(cache_it->second);
cache.erase(cache_it);
}
}
auto& agents = get_printer_agents();
auto agent_it = agents.find(info.id);
if (agent_it != agents.end() && agent_it->second.plugin_identifier != plugin_full_ref) {
BOOST_LOG_TRIVIAL(warning) << "Printer-agent plugin '" << capability_name << "' uses agent ID '" << info.id
<< "' already registered by '" << agent_it->second.display_name << "'";
return;
}
agents.insert_or_assign(info.id, PrinterAgentInfo(info.id, info.name, plugin_full_ref, std::move(factory)));
python_agent_ids[capability_key] = info.id;
auto& cache = get_printer_agent_cache();
auto cache_it = cache.find(info.id);
if (cache_it != cache.end()) {
cached_agent = std::move(cache_it->second);
cache.erase(cache_it);
}
}
if (cached_agent)
cached_agent->disconnect_printer();
BOOST_LOG_TRIVIAL(info) << "Registered printer-agent plugin '" << capability_name << "' with agent ID '" << info.id
<< "' and plugin ref '" << plugin_full_ref << "'";
}
void NetworkAgentFactory::deregister_python_plugin(const std::string& plugin_key)
{
std::vector<std::string> capability_names;
{
std::lock_guard<std::mutex> lock(s_registry_mutex);
for (const auto& [capability_key, token] : s_python_printer_agent_registration_tokens) {
(void) token;
if (capability_key.first == plugin_key)
capability_names.push_back(capability_key.second);
}
for (const auto& [capability_key, agent_id] : get_python_printer_agent_ids()) {
(void) agent_id;
if (capability_key.first == plugin_key &&
std::find(capability_names.begin(), capability_names.end(), capability_key.second) == capability_names.end())
capability_names.push_back(capability_key.second);
}
}
for (const std::string& capability_name : capability_names)
deregister_python_printer_agent(plugin_key, capability_name);
if (capability_names.empty())
BOOST_LOG_TRIVIAL(debug) << "Python printer-agent plugin '" << plugin_key << "' has no registered capabilities";
}
void NetworkAgentFactory::deregister_python_printer_agent(const std::string& plugin_key, const std::string& capability_name)
{
std::string agent_id;
std::shared_ptr<IPrinterAgent> cached_agent;
{
std::lock_guard<std::mutex> lock(s_registry_mutex);
auto token_it = std::find_if(s_python_printer_agent_registration_tokens.begin(),
s_python_printer_agent_registration_tokens.end(),
[&plugin_key, &capability_name](const auto& entry) {
return entry.first.first == plugin_key &&
entry.first.second == capability_name;
});
if (token_it != s_python_printer_agent_registration_tokens.end())
s_python_printer_agent_registration_tokens.erase(token_it);
auto& python_agent_ids = get_python_printer_agent_ids();
auto id_it = std::find_if(python_agent_ids.begin(), python_agent_ids.end(),
[&plugin_key, &capability_name](const auto& entry) {
return entry.first.first == plugin_key &&
entry.first.second == capability_name;
});
if (id_it == python_agent_ids.end()) {
BOOST_LOG_TRIVIAL(debug) << "Python printer-agent capability '" << capability_name << "' from plugin '"
<< plugin_key << "' is not registered";
return;
}
agent_id = id_it->second;
python_agent_ids.erase(id_it);
auto& cache = get_printer_agent_cache();
auto cache_it = cache.find(agent_id);
if (cache_it != cache.end()) {
cached_agent = std::move(cache_it->second);
cache.erase(cache_it);
}
get_printer_agents().erase(agent_id);
}
if (cached_agent)
cached_agent->disconnect_printer();
BOOST_LOG_TRIVIAL(info) << "Deregistered Python printer-agent capability '" << capability_name << "' from plugin '"
<< plugin_key << "' with agent ID '" << agent_id << "'";
}
bool NetworkAgentFactory::is_current_printer_agent_plugin()
{
auto* preset_bundle = GUI::wxGetApp().preset_bundle;
if (!preset_bundle)
return false;
std::string agent_key = ORCA_PRINTER_AGENT_ID;
if (preset_bundle->is_bbl_vendor())
agent_key = BBL_PRINTER_AGENT_ID;
const auto& cfg = preset_bundle->printers.get_edited_preset().config;
if (cfg.has("printer_agent")) {
const std::string& value = cfg.option<ConfigOptionString>("printer_agent")->value;
if (!value.empty())
agent_key = value;
}
const PrinterAgentInfo* info = get_printer_agent_info(agent_key);
return info && info->is_plugin();
}
} // namespace Slic3r
+26 -6
View File
@@ -24,13 +24,19 @@ using PrinterAgentFactory =
// Information about a registered printer agent
struct PrinterAgentInfo
{
std::string id; // e.g., "orca", "bbl"
std::string id; // Registry/config key, e.g. "orca" or a plugin AgentInfo::id
std::string display_name; // e.g., "Orca Native", "Bambu Lab"
std::string plugin_identifier; // Empty for built-ins, otherwise <plugin_key>;<uuid>;<capability_name>
PrinterAgentFactory factory; // Function to create the agent
bool is_plugin() const { return !plugin_identifier.empty(); }
PrinterAgentInfo(const std::string& id_, const std::string& display_name_, PrinterAgentFactory factory_)
: id(id_), display_name(display_name_), factory(std::move(factory_))
{}
PrinterAgentInfo(const std::string& id_, const std::string& display_name_, const std::string& plugin_identifier, PrinterAgentFactory factory_)
: id(id_), display_name(display_name_), plugin_identifier(plugin_identifier), factory(std::move(factory_))
{}
};
/**
@@ -80,27 +86,32 @@ public:
static bool register_printer_agent(const std::string& id, const std::string& display_name, PrinterAgentFactory factory);
/**
* Check if an agent ID is registered
* Check if an agent registry/config key is registered
*/
static bool is_printer_agent_registered(const std::string& id);
/**
* Get info about a registered agent
* Get info about a registered agent by registry/config key
*/
static const PrinterAgentInfo* get_printer_agent_info(const std::string& id);
/**
* Return the full plugin reference for a plugin-backed printer agent ID, or empty for built-ins.
*/
static std::string get_printer_agent_plugin_identifier(const std::string& id);
/**
* Get all registered printer agents (for UI population)
*/
static std::vector<PrinterAgentInfo> get_registered_printer_agents();
/**
* Create a printer agent by ID (using registry)
* Create a printer agent by registry/config key
*
* Returns a cached instance if one exists for the given ID, otherwise
* Returns a cached instance if one exists for the given key, otherwise
* creates a new agent via the registered factory and caches it.
*
* @param id Agent ID to create
* @param id Agent registry/config key to create
* @param cloud_agent Cloud agent for token access
* @param log_dir Directory for log files
* @return Shared pointer to IPrinterAgent, or nullptr if ID not found
@@ -148,6 +159,15 @@ public:
return nullptr;
}
// Plugin printer agents
static void register_python_plugin(const std::string& plugin_key);
static void deregister_python_plugin(const std::string& plugin_key);
static void register_python_printer_agent(const std::string& plugin_key, const std::string& capability_name);
static void deregister_python_printer_agent(const std::string& plugin_key, const std::string& capability_name);
static bool is_current_printer_agent_plugin();
private:
// Factory is not instantiable
NetworkAgentFactory() = delete;
File diff suppressed because it is too large. Load diff
+36 -1
View File
@@ -2,14 +2,17 @@
#define __ORCA_CLOUD_SERVICE_AGENT_HPP__
#include "ICloudServiceAgent.hpp"
#include <cstdlib>
#include <string>
#include <map>
#include <mutex>
#include <memory>
#include <atomic>
#include <chrono>
#include <functional>
#include <memory>
#include <thread>
#include <unordered_map>
#include <vector>
#include <nlohmann/json.hpp>
class wxSecretStore;
@@ -19,6 +22,22 @@ namespace Slic3r {
// Forward declarations
class AppConfig;
struct BundleMetadata;
struct PluginDescriptor;
struct PluginChangelog;
struct PluginDownloadData
{
std::string plugin_id;
std::string download_link;
std::string requested_os;
std::string returned_os;
};
struct PluginDownloadNotFound
{
std::string id;
std::string reason;
};
// Outcome of a token-refresh attempt: decides whether a 401 should log the user
// out (AuthRejected) or be treated as a recoverable condition (Transient).
@@ -270,6 +289,22 @@ public:
int get_subscribed_bundles(std::vector<std::pair<std::string, std::string>>* bundles,std::vector<std::string>& notfound, std::vector<std::string>& unauthorized);
int get_shared_bundle(const std::string& bundle_id, std::map<std::string, std::map<std::string, std::string>>* presets, BundleMetadata* bundle_metadata);
// ========================================================================
// Plugins API
// ========================================================================
int fetch_subscribed_manifests_into_descriptors(std::vector<PluginDescriptor>& descriptors, std::vector<std::string>& not_found, std::vector<std::string>& unauthorized);
int fetch_mine_manifests_into_descriptors(std::vector<PluginDescriptor>& descriptors);
int get_plugin_download_url(const std::string& uuid,
const std::string& requested_version,
std::vector<PluginDownloadData>& data,
std::vector<PluginDownloadNotFound>& not_found,
std::vector<std::string>& unauthorized);
std::string get_plugin_url(const std::string& sharing_token) const;
int subscribe_plugin(const std::string& plugin_uuid);
int unsubscribe_plugins(const std::vector<std::string>& plugin_uuids);
int delete_my_plugin(const std::string& plugin_uuid);
int fetch_plugin_changelogs(const std::vector<std::string>& uuids, std::unordered_map<std::string, std::vector<PluginChangelog>>& changelog);
// ========================================================================
// Additional Public Methods - Auth
// ========================================================================
+347
View File
@@ -0,0 +1,347 @@
#include "CloudPluginService.hpp"
#include "OrcaCloudServiceAgent.hpp"
#include "slic3r/Utils/Http.hpp"
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <cstddef>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
namespace Slic3r {
void CloudPluginService::set_cloud_agent(std::shared_ptr<OrcaCloudServiceAgent> agent) { m_orca_agent = std::move(agent); }
std::shared_ptr<OrcaCloudServiceAgent> CloudPluginService::get_cloud_agent() const { return m_orca_agent; }
bool CloudPluginService::can_fetch_cloud_plugins() const
{
if (!m_orca_agent) {
BOOST_LOG_TRIVIAL(warning) << "Orca service agent is null";
return false;
}
if (!m_orca_agent->is_user_login() || m_orca_agent->get_user_id().empty()) {
BOOST_LOG_TRIVIAL(info) << "User not logged in, no cloud directory";
return false;
}
return true;
}
bool CloudPluginService::fetch_manifests_into_descriptors(std::vector<PluginDescriptor>& descriptors,
std::vector<std::string>& not_found,
std::vector<std::string>& unauthorized) const
{
descriptors.clear();
not_found.clear();
unauthorized.clear();
if (m_orca_agent) {
int ret = m_orca_agent->fetch_subscribed_manifests_into_descriptors(descriptors, not_found, unauthorized);
if (ret != 0) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get subscribed plugins.";
return false;
}
std::vector<PluginDescriptor> mine_descriptors;
ret = m_orca_agent->fetch_mine_manifests_into_descriptors(mine_descriptors);
if (ret != 0) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get owned plugins.";
return false;
}
descriptors.insert(descriptors.end(), mine_descriptors.begin(), mine_descriptors.end());
}
return true;
}
bool CloudPluginService::request_cloud_subscribe(const std::string& plugin_uuid, std::string& error) const
{
error.clear();
if (!m_orca_agent) {
error = "No cloud agent.";
return false;
}
if (plugin_uuid.empty()) {
error = "Cloud plugin key is missing UUID.";
return false;
}
if (m_orca_agent->subscribe_plugin(plugin_uuid) != 0) {
error = "Failed to subscribe to cloud plugin, see logs for more info.";
return false;
}
return true;
}
bool CloudPluginService::request_cloud_unsubscribe(const PluginDescriptor& plugin, std::string& error) const
{
if (!m_orca_agent) {
error = "No cloud agent.";
return false;
}
if (!plugin.is_cloud_plugin()) {
error = "Only cloud plugins can be unsubscribed.";
return false;
}
const std::string cloud_uuid = plugin.cloud_uuid();
if (cloud_uuid.empty()) {
error = "Cloud plugin key is missing UUID.";
return false;
}
int result = m_orca_agent->unsubscribe_plugins({cloud_uuid});
if (result != 0) {
error = "Failed to unsubscribe plugin, see logs for more info.";
return false;
}
return true;
}
bool CloudPluginService::request_cloud_delete(const PluginDescriptor& plugin, std::string& error) const
{
if (!m_orca_agent) {
error = "No cloud agent.";
return false;
}
if (!plugin.is_cloud_plugin()) {
error = "Only cloud plugins can be deleted.";
return false;
}
const std::string cloud_uuid = plugin.cloud_uuid();
if (cloud_uuid.empty()) {
error = "Cloud plugin key is missing UUID.";
return false;
}
if (!plugin.cloud.has_value() || !plugin.cloud->is_mine) {
error = "Only your own plugins can be deleted from the cloud.";
return false;
}
int result = m_orca_agent->delete_my_plugin(cloud_uuid);
if (result != 0) {
error = "Failed to delete plugin from cloud, see logs for more info.";
return false;
}
return true;
}
bool CloudPluginService::download_cloud_plugin(PluginDescriptor& entry,
const std::string& requested_version,
CloudPluginDownload& download,
std::string& error) const
{
namespace fs = boost::filesystem;
error.clear();
download = {};
// Look up the cloud plugin metadata in the catalog.
std::string download_url;
if (!entry.is_cloud_plugin()) {
error = "Plugin is not a cloud plugin";
return false;
}
const std::string entry_uuid = entry.cloud_uuid();
if (!m_orca_agent) {
error = "Cloud service agent is null";
return false;
}
std::vector<PluginDownloadData> data;
std::vector<PluginDownloadNotFound> not_found;
std::vector<std::string> unauthorized;
int result = m_orca_agent->get_plugin_download_url(entry_uuid, requested_version, data, not_found, unauthorized);
if (result != 0) {
error = "Failed to fetch download_url, result =" + std::to_string(result);
return false;
}
for (const std::string& plugin_uuid : unauthorized) {
if (plugin_uuid == entry_uuid) {
error = "You are not authorized to download this cloud plugin.";
return false;
}
}
for (const PluginDownloadNotFound& item : not_found) {
if (item.id == entry_uuid) {
if (item.reason == "requested os not found")
error = "No plugin package is available for this operating system.";
else if (item.reason == "requested version not found")
error = requested_version.empty() ? "The selected plugin version was not found." :
"Plugin version " + requested_version + " was not found.";
else if (item.reason == "requested plugin not found")
error = "Cloud plugin was not found.";
else if (!item.reason.empty())
error = "Cloud plugin download was not found: " + item.reason + ".";
else
error = "Cloud plugin download was not found.";
return false;
}
}
for (const PluginDownloadData& item : data) {
if (item.plugin_id == entry_uuid) {
download_url = item.download_link;
break;
}
}
if (download_url.empty()) {
error = "No download URL is available for this plugin. Please "
"check the logs for errors.";
return false;
}
// Download the plugin package.
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "Downloading cloud plugin " << entry_uuid << " from " << download_url;
std::string body;
unsigned http_status = 0;
Http http = Http::get(download_url);
http.timeout_connect(30)
.timeout_max(300)
.on_complete([&body, &http_status](std::string response_body, unsigned status) {
body = std::move(response_body);
http_status = status;
})
.on_error([&error](std::string response_body, std::string err, unsigned status) {
error = std::move(err);
if (!response_body.empty())
error += " — " + response_body;
})
.perform_sync();
if (!error.empty()) {
error = "Failed to download plugin: " + error;
return false;
}
if (http_status >= 400) {
error = "Plugin download failed with HTTP status " + std::to_string(http_status);
return false;
}
if (body.empty()) {
error = "Plugin download returned empty data.";
return false;
}
// Detect file format from content: PK magic bytes = zip/wheel, otherwise .py.
const bool is_wheel = (body.size() >= 4 && body[0] == 'P' && body[1] == 'K' && body[2] == '\x03' && body[3] == '\x04');
const std::string ext = is_wheel ? ".whl" : ".py";
const fs::path tmp_path = fs::temp_directory_path() / (fs::unique_path("cloud_plugin-%%%%-%%%%-%%%%-%%%%%%%").string() + ext);
{
boost::nowide::ofstream file(tmp_path.string(), std::ios::binary | std::ios::trunc);
if (!file) {
error = "Failed to create temporary file for plugin download.";
return false;
}
file.write(body.data(), body.size());
if (!file) {
error = "Failed to write plugin data to temporary file.";
return false;
}
}
download.package_path = tmp_path;
if (entry.cloud.has_value())
entry.cloud->update_available = false;
return true;
}
bool CloudPluginService::fetch_plugin_changelog(const PluginDescriptor& descriptor,
std::vector<PluginChangelog>& changelog,
std::string& error) const
{
error.clear();
changelog.clear();
std::string plugin_uuid = descriptor.cloud_uuid();
std::vector<PluginDescriptor> descriptors{descriptor};
std::unordered_map<std::string, std::vector<PluginChangelog>> changelogs;
bool result = fetch_plugin_changelog(descriptors, changelogs, error);
bool found_changelog = changelogs.find(plugin_uuid) != changelogs.end();
if (changelogs.empty() || !found_changelog) {
if (!error.empty())
return false;
error = "Failed to fetch changelogs for plugin " + descriptor.cloud_uuid();
return false;
}
changelog = std::move(changelogs[plugin_uuid]);
return result;
}
bool CloudPluginService::fetch_plugin_changelog(const std::vector<PluginDescriptor>& descriptors,
std::unordered_map<std::string, std::vector<PluginChangelog>>& changelog,
std::string& error) const
{
error.clear();
changelog.clear();
if (!m_orca_agent) {
error = "Cloud service agent is null.";
return false;
}
size_t descriptor_count = descriptors.size();
if (descriptor_count <= 0) {
return true;
}
std::vector<std::string> uuids;
uuids.reserve(descriptor_count);
for (const PluginDescriptor& descriptor : descriptors) {
const std::string uuid = descriptor.cloud_uuid();
if (!uuid.empty())
uuids.push_back(uuid);
}
if (uuids.empty())
return true;
int result = m_orca_agent->fetch_plugin_changelogs(uuids, changelog);
if (result != 0) {
error = "Failed to fetch one or more plugin changelogs. result=" + std::to_string(result);
BOOST_LOG_TRIVIAL(warning) << error;
return false;
}
return true;
}
} // namespace Slic3r
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include "PluginDescriptor.hpp"
#include <boost/filesystem/path.hpp>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace Slic3r {
class OrcaCloudServiceAgent;
struct CloudPluginDownload
{
boost::filesystem::path package_path;
};
class CloudPluginService
{
public:
void set_cloud_agent(std::shared_ptr<OrcaCloudServiceAgent> agent);
std::shared_ptr<OrcaCloudServiceAgent> get_cloud_agent() const;
bool can_fetch_cloud_plugins() const;
bool fetch_manifests_into_descriptors(std::vector<PluginDescriptor>& descriptors,
std::vector<std::string>& not_found,
std::vector<std::string>& unauthorized) const;
bool request_cloud_subscribe(const std::string& plugin_uuid, std::string& error) const;
bool request_cloud_unsubscribe(const PluginDescriptor& plugin, std::string& error) const;
bool request_cloud_delete(const PluginDescriptor& plugin, std::string& error) const;
bool download_cloud_plugin(PluginDescriptor& entry,
const std::string& requested_version,
CloudPluginDownload& download,
std::string& error) const;
bool fetch_plugin_changelog(const PluginDescriptor& descriptor, std::vector<PluginChangelog>& changelog, std::string& error) const;
bool fetch_plugin_changelog(const std::vector<PluginDescriptor>& descriptors,
std::unordered_map<std::string, std::vector<PluginChangelog>>& changelog,
std::string& error) const;
private:
std::shared_ptr<OrcaCloudServiceAgent> m_orca_agent = nullptr;
};
} // namespace Slic3r
+281
View File
@@ -0,0 +1,281 @@
#include "PluginAuditManager.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/log/trivial.hpp>
#include <cstdlib>
#include <utility>
namespace Slic3r {
// ---------------------------------------------------------------------------
// Path safety
// ---------------------------------------------------------------------------
bool is_inside_allowed_root(const std::filesystem::path& candidate, const std::filesystem::path& allowed_root)
{
namespace fs = std::filesystem;
std::error_code ec;
// Canonicalize both paths. weakly_canonical resolves symlinks but does
// NOT require the path to exist — it canonicalizes the prefix that exists
// and appends the non-existing tail lexically.
fs::path canon_candidate = fs::weakly_canonical(candidate, ec);
if (ec) {
// Fall back to lexically_normal + absolute
canon_candidate = fs::absolute(candidate, ec).lexically_normal();
if (ec)
canon_candidate = candidate;
}
fs::path canon_root = fs::weakly_canonical(allowed_root, ec);
if (ec) {
canon_root = fs::absolute(allowed_root, ec).lexically_normal();
if (ec)
canon_root = allowed_root;
}
// Component-wise comparison: the root must be a prefix of candidate,
// and the next component must not be ".." or missing.
auto cand_it = canon_candidate.begin();
auto cand_end = canon_candidate.end();
auto root_it = canon_root.begin();
auto root_end = canon_root.end();
// Consume matching components
while (root_it != root_end && cand_it != cand_end && *root_it == *cand_it) {
++root_it;
++cand_it;
}
// If we didn't consume the entire root, candidate is not inside it.
if (root_it != root_end)
return false;
// The remaining path components must not traverse upward.
for (auto it = cand_it; it != cand_end; ++it) {
if (*it == "..")
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// ScopedPluginAuditContext
// ---------------------------------------------------------------------------
thread_local std::string PluginAuditManager::m_current_plugin_key = "";
thread_local PluginAuditManager::AuditMode PluginAuditManager::m_audit_mode = PluginAuditManager::AuditMode::Loading;
thread_local std::vector<std::filesystem::path> PluginAuditManager::m_scoped_allowed_roots;
thread_local bool PluginAuditManager::m_has_last_violation = false;
thread_local AuditViolation PluginAuditManager::m_last_violation;
ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key, PluginAuditManager::AuditMode mode)
: m_previous_id(PluginAuditManager::instance().current_plugin())
, m_previous_mode(PluginAuditManager::instance().audit_mode())
, m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots)
{
PluginAuditManager::instance().set_current_plugin(plugin_key);
PluginAuditManager::instance().set_audit_mode(mode);
PluginAuditManager::m_scoped_allowed_roots.clear();
}
ScopedPluginAuditContext::~ScopedPluginAuditContext()
{
PluginAuditManager::instance().set_current_plugin(m_previous_id);
PluginAuditManager::instance().set_audit_mode(m_previous_mode);
PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots);
}
// ---------------------------------------------------------------------------
// PluginAuditManager
// ---------------------------------------------------------------------------
PluginAuditManager& PluginAuditManager::instance()
{
static PluginAuditManager mgr;
return mgr;
}
void PluginAuditManager::set_current_plugin(const std::string& plugin_key) { m_current_plugin_key = plugin_key; }
std::string PluginAuditManager::current_plugin() const { return m_current_plugin_key; }
void PluginAuditManager::clear_current_plugin() { m_current_plugin_key.clear(); }
void PluginAuditManager::add_global_allowed_root(const std::filesystem::path& root)
{
if (root.empty())
return;
std::lock_guard<std::mutex> lock(m_mutex);
m_global_allowed_roots.push_back(root);
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string();
}
void PluginAuditManager::add_scoped_allowed_root(const std::filesystem::path& root)
{
if (root.empty())
return;
m_scoped_allowed_roots.push_back(root);
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string();
}
// ---------------------------------------------------------------------------
// Audit mode
// ---------------------------------------------------------------------------
void PluginAuditManager::set_audit_mode(AuditMode mode) { m_audit_mode = mode; }
PluginAuditManager::AuditMode PluginAuditManager::audit_mode() const { return m_audit_mode; }
// ---------------------------------------------------------------------------
// Policy checks
// ---------------------------------------------------------------------------
AuditDecision PluginAuditManager::check_open(const std::string& path_str, const std::string& mode)
{
if (path_str.empty())
return {true, ""};
std::string plugin_key = current_plugin();
if (plugin_key.empty())
return {true, ""}; // not running inside a plugin context
// During import/loading, only block writes. Python must be able to read
// stdlib modules and the plugin file itself during import.
if (m_audit_mode == AuditMode::Loading) {
bool is_write = (mode.find('w') != std::string::npos || mode.find('a') != std::string::npos || mode.find('+') != std::string::npos);
if (!is_write)
return {true, ""};
}
namespace fs = std::filesystem;
fs::path candidate(path_str);
// Resolve relative paths against the current working directory
if (candidate.is_relative()) {
std::error_code ec;
candidate = fs::absolute(candidate, ec);
if (ec)
candidate = fs::path(path_str);
}
for (const auto& root : m_scoped_allowed_roots) {
if (is_inside_allowed_root(candidate, root)) {
return {true, ""};
}
}
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const auto& root : m_global_allowed_roots) {
if (is_inside_allowed_root(candidate, root)) {
return {true, ""};
}
}
}
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << candidate.string() << " open_mode=" << mode
<< " audit_mode=" << (m_audit_mode == AuditMode::Loading ? "Loading" : "Enforcing")
<< " plugin=" << plugin_key;
return {false, "outside allowed root"};
}
void PluginAuditManager::report_violation(const AuditViolation& violation)
{
m_last_violation = violation;
m_has_last_violation = true;
BOOST_LOG_TRIVIAL(warning) << "[AUDIT BLOCKED] plugin=" << violation.plugin_key << " event=" << violation.event_name
<< " path=" << violation.path.string() << " reason=" << violation.reason;
}
void PluginAuditManager::clear_last_violation()
{
m_has_last_violation = false;
m_last_violation = AuditViolation{};
}
bool PluginAuditManager::last_violation(AuditViolation& violation) const
{
if (!m_has_last_violation)
return false;
violation = m_last_violation;
return true;
}
// ---------------------------------------------------------------------------
// The C-level audit hook
// ---------------------------------------------------------------------------
int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data)
{
auto* mgr = static_cast<PluginAuditManager*>(user_data);
if (!mgr)
return 0;
std::string event_name(event ? event : "");
// Verbose logging of every audit event (can be noisy)
if (mgr->verbose_events) {
BOOST_LOG_TRIVIAL(debug) << "[AUDIT EVENT] " << event_name;
}
// extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html
// --- open event ---
if (event_name == "open") {
const char* path_cstr = nullptr;
const char* mode_cstr = nullptr;
int flags = 0;
// open(path, mode, flags) — path may be str, bytes, or int fd
if (!PyArg_ParseTuple(args, "s|si", &path_cstr, &mode_cstr, &flags)) {
PyErr_Clear(); // couldn't parse; allow
return 0;
}
std::string path_str(path_cstr ? path_cstr : "");
std::string mode_str(mode_cstr ? mode_cstr : "r");
AuditDecision decision = mgr->check_open(path_str, mode_str);
if (!decision.allowed) {
AuditViolation violation;
violation.plugin_key = mgr->current_plugin();
violation.event_name = event_name;
violation.path = path_str;
violation.reason = decision.reason;
mgr->report_violation(violation);
PyErr_SetString(PyExc_PermissionError, "Plugin attempted to access a blocked file path");
return -1;
}
return 0;
}
// Unknown event — allow by default
return 0;
}
void PluginAuditManager::install_hook()
{
if (PySys_AddAuditHook(audit_hook, this) < 0) {
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook";
return;
}
BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully";
// data_dir() is the only globally-allowed root during enforced plugin execution.
// The executable directory and resources directory are intentionally NOT allowed
// here: plugins must not write outside data_dir() (G-code plugins additionally get
// the temp G-code folder via a scoped root). Reads remain permissive in Loading mode.
add_global_allowed_root(data_dir());
}
} // namespace Slic3r
+108
View File
@@ -0,0 +1,108 @@
#ifndef slic3r_PluginAuditManager_hpp_
#define slic3r_PluginAuditManager_hpp_
#include <Python.h>
#include <filesystem>
#include <mutex>
#include <string>
#include <vector>
namespace Slic3r {
struct AuditDecision {
bool allowed = true;
std::string reason;
};
struct AuditViolation {
std::string plugin_key;
std::string event_name;
std::filesystem::path path;
std::string reason;
};
// Returns true if candidate resolves to a path inside allowed_root.
// Uses weakly_canonical and component-wise comparison to reject traversal attacks.
bool is_inside_allowed_root(const std::filesystem::path& candidate,
const std::filesystem::path& allowed_root);
class PluginAuditManager
{
public:
static PluginAuditManager& instance();
// Call once after Py_Initialize to install the global audit hook.
void install_hook();
// --- current-plugin context (thread_local) ---
void set_current_plugin(const std::string& plugin_key);
std::string current_plugin() const;
void clear_current_plugin();
// --- allowed-roots registry ---
void add_global_allowed_root(const std::filesystem::path& root);
void add_scoped_allowed_root(const std::filesystem::path& root);
// --- enforcement mode ---
enum class AuditMode {
// Import/loading phase: allow reads anywhere, only block writes
// outside allowed roots. Python needs to read stdlib modules
// during import and those are not inside plugin directories.
Loading,
// Execution phase: block both reads and writes outside allowed
// roots, plus subprocess/socket/ctypes.
Enforcing,
};
void set_audit_mode(AuditMode mode);
AuditMode audit_mode() const;
// --- policy checks ---
AuditDecision check_open(const std::string& path, const std::string& mode);
void report_violation(const AuditViolation& violation);
void clear_last_violation();
bool last_violation(AuditViolation& violation) const;
bool verbose_events = true;
private:
friend class ScopedPluginAuditContext;
PluginAuditManager() = default;
static int audit_hook(const char* event, PyObject* args, void* user_data);
static thread_local std::string m_current_plugin_key;
static thread_local AuditMode m_audit_mode;
static thread_local std::vector<std::filesystem::path> m_scoped_allowed_roots;
static thread_local bool m_has_last_violation;
static thread_local AuditViolation m_last_violation;
std::mutex m_mutex;
std::vector<std::filesystem::path> m_global_allowed_roots;
};
// RAII guard that sets the current plugin key and restores the previous one.
class ScopedPluginAuditContext
{
public:
explicit ScopedPluginAuditContext(
const std::string& plugin_key,
PluginAuditManager::AuditMode mode = PluginAuditManager::AuditMode::Loading);
~ScopedPluginAuditContext();
ScopedPluginAuditContext(const ScopedPluginAuditContext&) = delete;
ScopedPluginAuditContext& operator=(const ScopedPluginAuditContext&) = delete;
private:
std::string m_previous_id;
PluginAuditManager::AuditMode m_previous_mode;
std::vector<std::filesystem::path> m_previous_scoped_roots;
};
} // namespace Slic3r
#endif // slic3r_PluginAuditManager_hpp_
+714
View File
@@ -0,0 +1,714 @@
#include "PluginCatalog.hpp"
#include "PluginFsUtils.hpp"
#include "libslic3r/Semver.hpp"
#include "libslic3r/Utils.hpp"
#include "PythonFileUtils.hpp"
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <chrono>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <thread>
namespace Slic3r {
namespace {
const char* kCloudPluginNotFoundError = "Plugin was not found in the cloud.";
bool is_cloud_version_newer(const std::string& cloud_version, const std::string& local_version)
{
auto cloud_parsed = Semver::parse(cloud_version);
auto local_parsed = Semver::parse(local_version);
if (cloud_parsed && local_parsed)
return *cloud_parsed > *local_parsed;
// Fall back to string comparison if semver parsing fails for either version.
return cloud_version != local_version;
}
void remove_plugin_from_entries(std::vector<PluginDescriptor>& entries, const std::string& plugin_key)
{
entries.erase(std::remove_if(entries.begin(), entries.end(), [&plugin_key](const PluginDescriptor& entry) {
return entry.plugin_key == plugin_key;
}), entries.end());
}
void clear_plugin_cloud_state_in_entries(std::vector<PluginDescriptor>& entries, const std::string& plugin_key)
{
for (auto& entry : entries) {
if (entry.plugin_key == plugin_key) {
entry.cloud.reset();
if (entry.normalized_error() == kCloudPluginNotFoundError)
entry.clear_error();
return;
}
}
}
bool set_plugin_error_in_entries(std::vector<PluginDescriptor>& entries, const std::string& plugin_key, const std::string& error)
{
for (auto& entry : entries) {
if (entry.plugin_key == plugin_key) {
entry.set_error(error);
return true;
}
}
return false;
}
void clear_plugin_errors_in_entries(std::vector<PluginDescriptor>& entries)
{
for (auto& entry : entries)
entry.clear_error();
}
// Derive a discovered descriptor's operational plugin_key: "<name>:<uuid>" for cloud
// entries, otherwise the (escaped) stem of name_source (the entry file when one
// exists, or the plugin directory when it does not). plugin_key is always derived,
// never read back from the install-state sidecar.
void assign_discovered_plugin_key(PluginDescriptor& descriptor, const boost::filesystem::path& name_source)
{
if (descriptor.is_cloud_plugin())
descriptor.plugin_key = descriptor.cloud_uuid();
else
descriptor.plugin_key = make_local_plugin_key(name_source.stem().string());
}
} // namespace
void PluginCatalog::discover_plugins(bool async, bool clear)
{
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_discovery_in_progress) {
BOOST_LOG_TRIVIAL(debug) << "Plugin discovery already running";
return;
}
if (clear) {
m_plugin_catalog.clear();
m_invalid_plugins.clear();
m_install_states.clear();
}
m_discovery_in_progress = true;
m_discovery_complete = false;
m_discovery_error.clear();
}
run_discovery(async);
}
bool PluginCatalog::is_discovery_complete() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_discovery_complete;
}
bool PluginCatalog::is_discovery_in_progress() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_discovery_in_progress;
}
std::string PluginCatalog::get_discovery_error() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_discovery_error;
}
bool PluginCatalog::wait_for_discovery(std::chrono::milliseconds timeout, std::string& error) const
{
std::unique_lock<std::mutex> lock(m_mutex);
if (!m_discovery_in_progress)
return true;
if (timeout == std::chrono::milliseconds::max()) {
m_discovery_cv.wait(lock, [this]() { return !m_discovery_in_progress; });
return true;
}
if (!m_discovery_cv.wait_for(lock, timeout, [this]() { return !m_discovery_in_progress; })) {
error = "Plugin discovery is still running";
return false;
}
return true;
}
const std::vector<PluginDescriptor>& PluginCatalog::get_plugin_catalog() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_plugin_catalog;
}
std::vector<PluginDescriptor> PluginCatalog::get_all_plugin_descriptors() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_plugin_catalog;
}
std::vector<PluginDescriptor> PluginCatalog::get_invalid_plugins() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_invalid_plugins;
}
std::vector<PluginDescriptor> PluginCatalog::get_plugin_descriptors_by_type(const std::string& type) const
{
return get_plugin_descriptors_by_type(plugin_capability_type_from_string(type));
}
std::vector<PluginDescriptor> PluginCatalog::get_plugin_descriptors_by_type(PluginCapabilityType type) const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<PluginDescriptor> result;
for (const auto& entry : m_plugin_catalog) {
if (entry.has_capability_type(type))
result.push_back(entry);
}
return result;
}
const PluginDescriptor* PluginCatalog::find_valid_plugin_descriptor(const std::string& plugin_key) const
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const auto& entry : m_plugin_catalog) {
if (entry.plugin_key == plugin_key)
return &entry;
}
return nullptr;
}
bool PluginCatalog::try_get_valid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto it = std::find_if(m_plugin_catalog.begin(), m_plugin_catalog.end(), [&plugin_key](const PluginDescriptor& entry) {
return entry.plugin_key == plugin_key;
});
if (it == m_plugin_catalog.end())
return false;
descriptor = *it;
return true;
}
bool PluginCatalog::try_get_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto find_by_key = [&plugin_key](const PluginDescriptor& entry) {
return entry.plugin_key == plugin_key;
};
auto catalog_it = std::find_if(m_plugin_catalog.begin(), m_plugin_catalog.end(), find_by_key);
if (catalog_it != m_plugin_catalog.end()) {
descriptor = *catalog_it;
return true;
}
auto invalid_it = std::find_if(m_invalid_plugins.begin(), m_invalid_plugins.end(), find_by_key);
if (invalid_it != m_invalid_plugins.end()) {
descriptor = *invalid_it;
return true;
}
return false;
}
bool PluginCatalog::try_get_invalid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto it = std::find_if(m_invalid_plugins.begin(), m_invalid_plugins.end(), [&plugin_key](const PluginDescriptor& entry) {
return entry.plugin_key == plugin_key;
});
if (it == m_invalid_plugins.end())
return false;
descriptor = *it;
return true;
}
bool PluginCatalog::has_valid_plugin_descriptor(const std::string& plugin_key) const
{
std::lock_guard<std::mutex> lock(m_mutex);
return std::any_of(m_plugin_catalog.begin(), m_plugin_catalog.end(), [&plugin_key](const PluginDescriptor& entry) {
return entry.plugin_key == plugin_key;
});
}
void PluginCatalog::set_cloud_plugin_dir(const std::string& dir)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_cloud_plugin_dir = dir;
}
std::vector<std::string> PluginCatalog::get_plugin_directories() const
{
namespace fs = boost::filesystem;
std::vector<std::string> dirs;
std::string cloud_plugin_dir_name;
{
std::lock_guard<std::mutex> lock(m_mutex);
cloud_plugin_dir_name = m_cloud_plugin_dir;
}
auto add_or_create_dir = [&dirs](const fs::path& path) {
if (fs::exists(path) && fs::is_directory(path)) {
dirs.push_back(path.string());
} else {
try {
fs::create_directories(path);
dirs.push_back(path.string());
BOOST_LOG_TRIVIAL(info) << "Created plugin directory: " << path.string();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "Failed to create plugin directory: " << ex.what();
}
}
};
// Local plugins: {data_dir}/orca_plugins/
add_or_create_dir(fs::path(data_dir()) / "orca_plugins");
// Cloud plugins: {data_dir}/orca_plugins/_subscribed/{user_id}/
if (!cloud_plugin_dir_name.empty())
add_or_create_dir(fs::path(get_cloud_plugin_dir(cloud_plugin_dir_name)));
return dirs;
}
void PluginCatalog::update_cloud_catalog(const std::vector<PluginDescriptor>& cloud_list)
{
std::lock_guard<std::mutex> lock(m_mutex);
for (const auto& cloud_entry : cloud_list) {
std::string cloud_uuid = cloud_entry.cloud_uuid();
const std::string cloud_key = cloud_entry.plugin_key;
if (cloud_uuid.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Skipping cloud plugin record without UUID";
continue;
}
auto matches_cloud_descriptor = [&cloud_key, &cloud_uuid](const PluginDescriptor& entry) {
if (!cloud_key.empty() && entry.plugin_key == cloud_key)
return true;
return entry.is_cloud_plugin() && entry.cloud_uuid() == cloud_uuid;
};
auto apply_cloud_state = [&cloud_entry, &cloud_uuid](PluginDescriptor& entry) {
const PluginDescriptor local_entry = entry;
const std::string installed_version = entry.installed_version;
const std::string latest_version = cloud_entry.latest_available_version();
const std::string local_plugin_root = entry.plugin_root;
const std::string local_entry_path = entry.entry_path;
const bool local_metadata_valid = entry.is_metadata_valid();
const bool has_local_package = entry.has_local_package();
const std::string previous_error = entry.error;
entry = cloud_entry;
entry.plugin_root = local_plugin_root;
entry.entry_path = local_entry_path;
if (has_local_package)
apply_plugin_metadata_fallbacks(entry, local_entry);
if (entry.plugin_key.empty())
entry.plugin_key = cloud_uuid;
entry.metadata_valid = has_local_package ? local_metadata_valid : cloud_entry.metadata_valid;
entry.error = previous_error;
if (!entry.cloud.has_value())
entry.cloud = CloudPluginState{cloud_uuid, has_local_package, false, false};
else if (entry.cloud->uuid.empty())
entry.cloud->uuid = cloud_uuid;
entry.cloud->installed = has_local_package;
// The installed version is the source of truth read back from the install-state
// sidecar (the version fetched from the cloud at install time), not the local
// manifest/PEP723 header. The header may be stale — the cloud can bump the version
// without the header changing — which would otherwise make an already-updated
// plugin appear perpetually out of date.
entry.installed_version = has_local_package ? installed_version : std::string{};
entry.cloud->update_available = has_local_package && local_metadata_valid && !installed_version.empty() &&
!latest_version.empty() && is_cloud_version_newer(latest_version, installed_version);
if (entry.normalized_error() == kCloudPluginNotFoundError)
entry.clear_error();
};
auto catalog_it = std::find_if(m_plugin_catalog.begin(), m_plugin_catalog.end(), matches_cloud_descriptor);
if (catalog_it != m_plugin_catalog.end()) {
apply_cloud_state(*catalog_it);
continue;
}
auto invalid_it = std::find_if(m_invalid_plugins.begin(), m_invalid_plugins.end(), matches_cloud_descriptor);
if (invalid_it != m_invalid_plugins.end()) {
apply_cloud_state(*invalid_it);
continue;
}
PluginDescriptor normalized_entry = cloud_entry;
if (normalized_entry.plugin_key.empty())
normalized_entry.plugin_key = cloud_uuid;
if (!normalized_entry.cloud.has_value())
normalized_entry.cloud = CloudPluginState{cloud_uuid, false, false, false};
else if (normalized_entry.cloud->uuid.empty())
normalized_entry.cloud->uuid = cloud_uuid;
m_plugin_catalog.push_back(std::move(normalized_entry));
}
}
void PluginCatalog::mark_cloud_plugin_unauthorized(const std::string& cloud_uuid)
{
std::lock_guard<std::mutex> lock(m_mutex);
auto mark_unauthorized = [&cloud_uuid](std::vector<PluginDescriptor>& entries) {
for (auto& entry : entries) {
if (entry.is_cloud_plugin() && entry.cloud_uuid() == cloud_uuid) {
entry.set_unauthorized(true);
if (entry.cloud.has_value())
entry.cloud->update_available = false;
return true;
}
}
return false;
};
if (mark_unauthorized(m_plugin_catalog))
return;
mark_unauthorized(m_invalid_plugins);
}
void PluginCatalog::mark_cloud_plugin_not_found(const std::string& cloud_uuid)
{
std::lock_guard<std::mutex> lock(m_mutex);
auto mark_not_found = [&cloud_uuid](std::vector<PluginDescriptor>& entries) {
for (auto& entry : entries) {
if (entry.is_cloud_plugin() && entry.cloud_uuid() == cloud_uuid) {
if (!entry.has_local_package())
entry.set_error(kCloudPluginNotFoundError);
return true;
}
}
return false;
};
if (mark_not_found(m_plugin_catalog))
return;
mark_not_found(m_invalid_plugins);
}
void PluginCatalog::clear_cloud_plugin_unauthorized()
{
std::lock_guard<std::mutex> lock(m_mutex);
auto clear_unauthorized = [](std::vector<PluginDescriptor>& entries) {
for (auto& entry : entries) {
if (entry.is_cloud_plugin())
entry.set_unauthorized(false);
}
};
clear_unauthorized(m_plugin_catalog);
clear_unauthorized(m_invalid_plugins);
}
void PluginCatalog::clear_cloud_plugin_not_found_errors()
{
std::lock_guard<std::mutex> lock(m_mutex);
auto clear_not_found_errors = [](std::vector<PluginDescriptor>& entries) {
for (auto& entry : entries) {
if (entry.is_cloud_plugin() && entry.normalized_error() == kCloudPluginNotFoundError)
entry.clear_error();
}
};
clear_not_found_errors(m_plugin_catalog);
clear_not_found_errors(m_invalid_plugins);
}
void PluginCatalog::clear_cloud_plugin_catalog()
{
std::lock_guard<std::mutex> lock(m_mutex);
auto clear_entries = [](std::vector<PluginDescriptor>& entries) {
entries.erase(std::remove_if(entries.begin(), entries.end(), [](const PluginDescriptor& entry) {
return entry.is_cloud_plugin() && !entry.has_local_package();
}), entries.end());
for (auto& entry : entries) {
if (entry.is_cloud_plugin()) {
entry.cloud->update_available = false;
entry.cloud->unauthorized = false;
entry.cloud->is_mine = false;
if (!entry.plugin_root.empty() || !entry.entry_path.empty())
entry.cloud->installed = true;
}
if (entry.normalized_error() == kCloudPluginNotFoundError)
entry.clear_error();
}
};
clear_entries(m_plugin_catalog);
clear_entries(m_invalid_plugins);
BOOST_LOG_TRIVIAL(info) << "Cleared cloud plugin catalog entries";
}
void PluginCatalog::remove_plugin(const std::string& plugin_key)
{
std::lock_guard<std::mutex> lock(m_mutex);
remove_plugin_from_entries(m_plugin_catalog, plugin_key);
remove_plugin_from_entries(m_invalid_plugins, plugin_key);
}
void PluginCatalog::clear_plugin_cloud_state(const std::string& plugin_key)
{
std::lock_guard<std::mutex> lock(m_mutex);
clear_plugin_cloud_state_in_entries(m_plugin_catalog, plugin_key);
clear_plugin_cloud_state_in_entries(m_invalid_plugins, plugin_key);
}
bool PluginCatalog::set_plugin_error(const std::string& plugin_key, const std::string& error)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (set_plugin_error_in_entries(m_plugin_catalog, plugin_key, error))
return true;
return set_plugin_error_in_entries(m_invalid_plugins, plugin_key, error);
}
bool PluginCatalog::clear_plugin_error(const std::string& plugin_key)
{
return set_plugin_error(plugin_key, "");
}
void PluginCatalog::clear_all_plugin_errors()
{
std::lock_guard<std::mutex> lock(m_mutex);
clear_plugin_errors_in_entries(m_plugin_catalog);
clear_plugin_errors_in_entries(m_invalid_plugins);
}
void PluginCatalog::discover_plugins_impl()
{
const auto start_time = std::chrono::steady_clock::now();
try {
const std::vector<std::string> plugin_dirs = get_plugin_directories();
BOOST_LOG_TRIVIAL(info) << "Scanning " << plugin_dirs.size() << " plugin directories...";
for (const auto& dir : plugin_dirs)
scan_directory(dir);
std::size_t plugin_count = 0;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_discovery_complete = true;
plugin_count = m_plugin_catalog.size();
}
const auto end_time = std::chrono::steady_clock::now();
const auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
BOOST_LOG_TRIVIAL(info) << "Plugin discovery completed in " << duration.count() << "ms. Found " << plugin_count
<< " plugin manifests";
} catch (const std::exception& ex) {
std::lock_guard<std::mutex> lock(m_mutex);
m_discovery_error = std::string("Plugin discovery failed: ") + ex.what();
m_discovery_complete = true;
BOOST_LOG_TRIVIAL(error) << m_discovery_error;
}
}
void PluginCatalog::scan_directory(const std::string& dir_path)
{
namespace fs = boost::filesystem;
if (!fs::exists(dir_path) || !fs::is_directory(dir_path))
return;
BOOST_LOG_TRIVIAL(debug) << "Scanning plugin directory: " << dir_path;
try {
for (fs::directory_iterator it(dir_path); it != fs::directory_iterator(); ++it) {
if (!fs::is_directory(it->status()))
continue;
const fs::path plugin_dir = it->path();
if (is_ignored_plugin_directory(plugin_dir))
continue;
PluginDescriptor descriptor;
descriptor.plugin_root = plugin_dir.string();
std::string entry_error;
const fs::path entry_path = find_installed_plugin_entry(plugin_dir, entry_error);
if (entry_path.empty()) {
descriptor.set_error(entry_error);
read_install_state(plugin_dir, descriptor);
assign_discovered_plugin_key(descriptor, plugin_dir);
PluginInstallState install_state;
const bool have_install_state = read_install_state(plugin_dir, install_state);
std::lock_guard<std::mutex> lock(m_mutex);
if (have_install_state)
m_install_states[descriptor.plugin_key] = std::move(install_state);
m_invalid_plugins.push_back(std::move(descriptor));
BOOST_LOG_TRIVIAL(warning) << "Invalid plugin package: " << plugin_dir.string() << " - " << m_invalid_plugins.back().error;
continue;
}
// Parse local file metadata for dependencies and loading details.
std::string meta_error;
if (entry_path.extension() == ".whl") {
if (!read_wheel_plugin_metadata(entry_path, descriptor, meta_error)) {
descriptor.set_error(meta_error);
read_install_state(plugin_dir, descriptor);
assign_discovered_plugin_key(descriptor, entry_path);
PluginInstallState install_state;
const bool have_install_state = read_install_state(plugin_dir, install_state);
std::lock_guard<std::mutex> lock(m_mutex);
if (have_install_state)
m_install_states[descriptor.plugin_key] = std::move(install_state);
m_invalid_plugins.push_back(std::move(descriptor));
BOOST_LOG_TRIVIAL(warning) << "Invalid wheel plugin: " << plugin_dir.string() << " - "
<< m_invalid_plugins.back().error;
continue;
}
descriptor.entry_path = entry_path.string();
} else {
if (!read_python_plugin_metadata(entry_path, descriptor, meta_error)) {
descriptor.set_error(meta_error);
read_install_state(plugin_dir, descriptor);
assign_discovered_plugin_key(descriptor, entry_path);
PluginInstallState install_state;
const bool have_install_state = read_install_state(plugin_dir, install_state);
std::lock_guard<std::mutex> lock(m_mutex);
if (have_install_state)
m_install_states[descriptor.plugin_key] = std::move(install_state);
m_invalid_plugins.push_back(std::move(descriptor));
BOOST_LOG_TRIVIAL(warning) << "Invalid .py plugin: " << plugin_dir.string() << " - " << m_invalid_plugins.back().error;
continue;
}
descriptor.entry_path = entry_path.string();
}
descriptor.set_metadata_valid(true);
descriptor.clear_error();
// Read cloud identity (uuid) from sidecar; plugin_key is always derived.
read_install_state(plugin_dir, descriptor);
assign_discovered_plugin_key(descriptor, entry_path);
PluginInstallState install_state;
const bool have_install_state = read_install_state(plugin_dir, install_state);
std::lock_guard<std::mutex> lock(m_mutex);
if (have_install_state)
m_install_states[descriptor.plugin_key] = std::move(install_state);
m_plugin_catalog.push_back(std::move(descriptor));
BOOST_LOG_TRIVIAL(info) << "Discovered plugin: " << m_plugin_catalog.back().name
<< " (type: " << m_plugin_catalog.back().type_label() << ", version: " << m_plugin_catalog.back().version
<< ")";
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "Error scanning directory " << dir_path << ": " << ex.what();
}
}
void PluginCatalog::run_discovery(bool async)
{
auto task = [this]() { run_discovery_task(); };
if (async)
std::thread(std::move(task)).detach();
else
task();
}
void PluginCatalog::run_discovery_task()
{
try {
discover_plugins_impl();
} catch (const std::exception& ex) {
{
std::lock_guard<std::mutex> lock(m_mutex);
m_discovery_error = std::string("Plugin discovery failed: ") + ex.what();
m_discovery_complete = true;
}
BOOST_LOG_TRIVIAL(error) << m_discovery_error;
} catch (...) {
{
std::lock_guard<std::mutex> lock(m_mutex);
m_discovery_error = "Plugin discovery failed: unknown error";
m_discovery_complete = true;
}
BOOST_LOG_TRIVIAL(error) << m_discovery_error;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_discovery_in_progress = false;
}
m_discovery_cv.notify_all();
}
bool PluginCatalog::update_plugin_descriptor(const std::string& plugin_key, const PluginDescriptor& descriptor)
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto find_by_key = [&plugin_key](const PluginDescriptor& entry) {
return entry.plugin_key == plugin_key;
};
auto catalog_it = std::find_if(m_plugin_catalog.begin(), m_plugin_catalog.end(), find_by_key);
if (catalog_it != m_plugin_catalog.end()) {
*catalog_it = std::move(descriptor);
return true;
}
auto invalid_it = std::find_if(m_invalid_plugins.begin(), m_invalid_plugins.end(), find_by_key);
if (invalid_it != m_invalid_plugins.end()) {
*invalid_it = std::move(descriptor);
return true;
}
return false;
}
bool PluginCatalog::try_get_install_state(const std::string& plugin_key, PluginInstallState& out) const
{
std::lock_guard<std::mutex> lock(m_mutex);
const auto it = m_install_states.find(plugin_key);
if (it == m_install_states.end())
return false;
out = it->second;
return true;
}
std::vector<std::string> PluginCatalog::get_enabled_plugin_keys() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<std::string> keys;
for (const auto& [plugin_key, state] : m_install_states) {
if (state.enabled)
keys.push_back(plugin_key);
}
return keys;
}
} // namespace Slic3r
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include "PluginDescriptor.hpp"
#include "PythonFileUtils.hpp"
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace Slic3r {
class PluginCatalog
{
public:
void discover_plugins(bool async = false, bool clear = false);
bool is_discovery_complete() const;
bool is_discovery_in_progress() const;
std::string get_discovery_error() const;
bool wait_for_discovery(std::chrono::milliseconds timeout, std::string& error) const;
const std::vector<PluginDescriptor>& get_plugin_catalog() const;
std::vector<PluginDescriptor> get_all_plugin_descriptors() const;
std::vector<PluginDescriptor> get_invalid_plugins() const;
std::vector<PluginDescriptor> get_plugin_descriptors_by_type(const std::string& type) const;
std::vector<PluginDescriptor> get_plugin_descriptors_by_type(PluginCapabilityType type) const;
const PluginDescriptor* find_valid_plugin_descriptor(const std::string& plugin_key) const;
bool try_get_valid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const;
bool try_get_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const;
bool try_get_invalid_plugin_descriptor(const std::string& plugin_key, PluginDescriptor& descriptor) const;
bool has_valid_plugin_descriptor(const std::string& plugin_key) const;
void set_cloud_plugin_dir(const std::string& dir);
std::vector<std::string> get_plugin_directories() const;
void update_cloud_catalog(const std::vector<PluginDescriptor>& cloud_list);
void mark_cloud_plugin_unauthorized(const std::string& cloud_uuid);
void mark_cloud_plugin_not_found(const std::string& cloud_uuid);
void clear_cloud_plugin_unauthorized();
void clear_cloud_plugin_not_found_errors();
void clear_cloud_plugin_catalog();
void remove_plugin(const std::string& plugin_key);
void clear_plugin_cloud_state(const std::string& plugin_key);
bool set_plugin_error(const std::string& plugin_key, const std::string& error);
bool clear_plugin_error(const std::string& plugin_key);
void clear_all_plugin_errors();
bool update_plugin_descriptor(const std::string& plugin_key, const PluginDescriptor& descriptor);
// Cached install state, populated from each plugin's .install_state.json during discovery.
bool try_get_install_state(const std::string& plugin_key, PluginInstallState& out) const;
std::vector<std::string> get_enabled_plugin_keys() const;
private:
void discover_plugins_impl();
void scan_directory(const std::string& dir_path);
void run_discovery(bool async);
void run_discovery_task();
bool m_discovery_complete = false;
std::string m_discovery_error;
bool m_discovery_in_progress = false;
std::vector<PluginDescriptor> m_plugin_catalog;
std::vector<PluginDescriptor> m_invalid_plugins;
std::unordered_map<std::string, PluginInstallState> m_install_states;
std::string m_cloud_plugin_dir;
mutable std::mutex m_mutex;
mutable std::condition_variable m_discovery_cv;
};
} // namespace Slic3r
+222
View File
@@ -0,0 +1,222 @@
#pragma once
#include "PythonPluginInterface.hpp"
#include <algorithm>
#include <cctype>
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
// Cloud overlay on a PluginDescriptor; presence means the descriptor is cloud-backed.
struct CloudPluginState
{
std::string uuid; // Cloud service UUID without the cloud: key prefix.
bool installed = false; // Cloud package exists locally and can be loaded.
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.
};
enum class PluginUpdateStatus
{
Normal,
UpdateAvailable,
Unauthorized,
};
struct PluginChangelog {
std::string changelog_id;
std::string plugin_uuid;
std::string version;
std::string changelog;
long long created_time = 0;
};
inline void sort_plugin_changelog(std::vector<PluginChangelog>& changelog)
{
std::sort(changelog.begin(), changelog.end(), [](const PluginChangelog& lhs, const PluginChangelog& rhs) {
if (lhs.created_time != rhs.created_time)
return lhs.created_time > rhs.created_time;
return lhs.version > rhs.version;
});
}
// Canonical plugin runtime/catalog representation used by Orca.
struct PluginDescriptor
{
std::string plugin_key; // OrcaSlicer-generated operational identity
std::string name; // Display name
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 installed_version; // Locally installed package version. Preserved across cloud merges, which overwrite `version` with the latest cloud version. Empty when not installed.
std::vector<PluginCapabilityType> capability_types; // Capability types this package materializes (one package → N capabilities)
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.
std::string entry_path; // Full path to the installed plugin entry file
std::string entry_package; // Import package/module used for package-based loading
std::vector<std::string> dependencies; // Python dependency requirements declared by plugin package metadata
std::vector<PluginChangelog> changelog; // Cloud release changelog, sorted newest-first when available.
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.
std::string sharing_token; // Use BASE_URL/p/SHARING_TOKEN to open relevant plugin in browser.
std::string thumbnail_url; // Cloud main_image pre-signed (access_url) thumbnail; empty for local plugins. Display-only.
bool is_cloud_plugin() const { return cloud.has_value(); }
std::string cloud_uuid() const { return cloud.has_value() ? cloud->uuid : std::string{}; }
bool has_local_package() const { return !is_cloud_plugin() || cloud->installed || !plugin_root.empty() || !entry_path.empty(); }
bool is_metadata_valid() const { return metadata_valid; }
// Capability-type helpers. A package may materialize several capability types;
// these accessors give callers that still reason about a single "type" (catalog
// display, cloud overlay, dispatch — finalized in later tasks) a stable view.
bool has_capability_type(PluginCapabilityType t) const
{
return std::find(capability_types.begin(), capability_types.end(), t) != capability_types.end();
}
PluginCapabilityType primary_capability_type() const
{
return capability_types.empty() ? PluginCapabilityType::Unknown : capability_types.front();
}
// Set the package to a single capability type (metadata/cloud sources currently
// declare one type; multi-type discovery is finalized in later tasks).
void set_capability_type(PluginCapabilityType t) { capability_types.assign(1, t); }
// Canonical label derived from the primary type for UI / config matching.
std::string type_label() const { return plugin_capability_type_to_string(primary_capability_type()); }
std::string normalized_error() const
{
auto begin = std::find_if_not(error.begin(), error.end(), [](unsigned char ch) { return std::isspace(ch) != 0; });
if (begin == error.end())
return {};
auto end = std::find_if_not(error.rbegin(), error.rend(), [](unsigned char ch) { return std::isspace(ch) != 0; }).base();
return std::string(begin, end);
}
bool has_error() const { return !normalized_error().empty(); }
PluginUpdateStatus get_update_status() const
{
if (!cloud.has_value())
return PluginUpdateStatus::Normal;
if (cloud->unauthorized)
return PluginUpdateStatus::Unauthorized;
if (cloud->update_available)
return PluginUpdateStatus::UpdateAvailable;
return PluginUpdateStatus::Normal;
}
bool has_update_available() const { return get_update_status() == PluginUpdateStatus::UpdateAvailable; }
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;
}
void set_metadata_valid(bool is_valid)
{
metadata_valid = is_valid;
if (!is_valid && !has_error())
error = "Plugin metadata is invalid.";
}
void set_unauthorized(bool unauthorized)
{
if (!cloud.has_value())
return;
cloud->unauthorized = unauthorized;
if (unauthorized)
cloud->update_available = false;
}
void clear_error() { error.clear(); }
void set_error(std::string message) { error = std::move(message); }
};
inline void apply_plugin_metadata_fallbacks(PluginDescriptor& target, const PluginDescriptor& fallback)
{
if (target.name.empty())
target.name = fallback.name;
if (target.description.empty())
target.description = fallback.description;
if (target.author.empty())
target.author = fallback.author;
if (target.version.empty())
target.version = fallback.version;
if (target.capability_types.empty())
target.capability_types = fallback.capability_types;
if (target.entry_package.empty())
target.entry_package = fallback.entry_package;
if (target.dependencies.empty())
target.dependencies = fallback.dependencies;
}
// Sanitize a value for use as a filesystem name and as a local plugin_key:
// keeps [A-Za-z0-9_-.], collapses any other run into a single '_'.
inline std::string filesystem_safe_escape(const std::string& value)
{
std::string escaped;
escaped.reserve(value.size());
for (unsigned char ch : value) {
if (std::isalnum(ch) || ch == '_' || ch == '-' || ch == '.') {
escaped += static_cast<char>(ch);
continue;
}
if (escaped.empty() || escaped.back() != '_')
escaped += '_';
}
return escaped.empty() ? "path" : escaped;
}
// Plugin display names are serialized into ';'-delimited config/preset strings
// (see escape_strings_cstyle in libslic3r/Config.cpp), so a ';' in a name would
// corrupt that encoding. Replace any ';' with '_' and otherwise leave the name
// untouched. Capability names are validated more strictly elsewhere — a ';' there
// is treated as an error because those names drive preset dispatch, not display.
inline std::string sanitize_plugin_name(std::string name)
{
std::replace(name.begin(), name.end(), ';', '_');
return name;
}
// True when s is a canonical 8-4-4-12 hex UUID, e.g. 550e8400-e29b-41d4-a716-446655440000.
inline bool is_uuid(const std::string& s)
{
if (s.size() != 36)
return false;
for (size_t i = 0; i < s.size(); ++i) {
const char ch = s[i];
if (i == 8 || i == 13 || i == 18 || i == 23) {
if (ch != '-')
return false;
} else if (std::isxdigit(static_cast<unsigned char>(ch)) == 0) {
return false;
}
}
return true;
}
// Key generation helpers.
// Local key = filesystem-safe plugin file stem (filename without extension), e.g. "Slow_Load_Plugin".
// Cloud key = the cloud UUID. Local vs cloud is determined from the descriptor's cloud
// state, never by parsing the key. Plugin keys are matched by plain equality.
inline std::string make_local_plugin_key(const std::string& stem)
{
return filesystem_safe_escape(stem);
}
} // namespace Slic3r
+110
View File
@@ -0,0 +1,110 @@
#include "PluginFsUtils.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <filesystem>
#include "PluginAuditManager.hpp"
namespace Slic3r {
const char* const INSTALL_STATE_FILE = ".install_state.json";
std::string get_cloud_plugin_dir(const std::string& user_id)
{
namespace fs = boost::filesystem;
return (fs::path(data_dir()) / "orca_plugins" / PLUGIN_SUBSCRIBED_DIR / user_id).string();
}
boost::filesystem::path resolve_plugin_root_from_descriptor(const PluginDescriptor& descriptor)
{
namespace fs = boost::filesystem;
if (!descriptor.plugin_root.empty())
return fs::path(descriptor.plugin_root);
if (!descriptor.entry_path.empty())
return fs::path(descriptor.entry_path).parent_path();
return {};
}
bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root,
const std::vector<std::string>& allowed_dirs)
{
boost::system::error_code ec;
boost::filesystem::path resolved_root = boost::filesystem::weakly_canonical(candidate_root, ec);
if (ec) {
ec.clear();
resolved_root = boost::filesystem::absolute(candidate_root, ec);
}
if (ec || resolved_root.empty())
return false;
for (const auto& allowed_dir : allowed_dirs) {
if (is_inside_allowed_root(std::filesystem::path(resolved_root.string()),
std::filesystem::path(allowed_dir)))
return true;
}
return false;
}
bool resolve_allowed_plugin_root(const PluginDescriptor& descriptor,
const std::vector<std::string>& allowed_dirs,
const std::string& out_of_scope_error,
boost::filesystem::path& resolved_root,
std::string& error)
{
namespace fs = boost::filesystem;
const fs::path plugin_root = resolve_plugin_root_from_descriptor(descriptor);
if (plugin_root.empty()) {
error = "Plugin folder could not be determined.";
return false;
}
boost::system::error_code ec;
resolved_root = fs::weakly_canonical(plugin_root, ec);
if (ec) {
ec.clear();
resolved_root = fs::absolute(plugin_root, ec);
}
if (ec || resolved_root.empty()) {
error = "Failed to resolve plugin folder: " + plugin_root.string();
return false;
}
if (!is_plugin_root_allowed(plugin_root, allowed_dirs)) {
error = out_of_scope_error;
return false;
}
return true;
}
bool delete_plugin_root(const boost::filesystem::path& resolved_root,
const std::string& plugin_id,
std::string& error)
{
namespace fs = boost::filesystem;
boost::system::error_code ec;
const auto removed_count = fs::remove_all(resolved_root, ec);
if (ec) {
error = "Failed to delete plugin folder " + resolved_root.string() + ": " + ec.message();
return false;
}
if (removed_count == 0) {
error = "Plugin folder was not found: " + resolved_root.string();
return false;
}
BOOST_LOG_TRIVIAL(info) << "Deleted plugin: " << plugin_id << " from " << resolved_root.string();
return true;
}
} // namespace Slic3r
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "PluginDescriptor.hpp"
#include <boost/filesystem/path.hpp>
#include <string>
#include <vector>
#define PLUGIN_SUBSCRIBED_DIR "_subscribed"
namespace Slic3r {
extern const char* const INSTALL_STATE_FILE;
// Returns the cloud plugin install/scan directory for a given user_id.
// Path: {data_dir}/orca_plugins/_subscribed/{user_id}/
std::string get_cloud_plugin_dir(const std::string& user_id);
boost::filesystem::path resolve_plugin_root_from_descriptor(const PluginDescriptor& descriptor);
bool is_plugin_root_allowed(const boost::filesystem::path& candidate_root,
const std::vector<std::string>& allowed_dirs);
bool resolve_allowed_plugin_root(const PluginDescriptor& descriptor,
const std::vector<std::string>& allowed_dirs,
const std::string& out_of_scope_error,
boost::filesystem::path& resolved_root,
std::string& error);
bool delete_plugin_root(const boost::filesystem::path& resolved_root,
const std::string& plugin_id,
std::string& error);
} // namespace Slic3r
+513
View File
@@ -0,0 +1,513 @@
#include "PluginHostApi.hpp"
#include "PluginHostUi.hpp"
#include <libslic3r/BoundingBox.hpp>
#include <libslic3r/Model.hpp>
#include <libslic3r/Preset.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <libslic3r/TriangleMesh.hpp>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/Plater.hpp>
#include <pybind11/numpy.h>
#include <pybind11/stl.h>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <vector>
namespace py = pybind11;
namespace Slic3r {
namespace {
GUI::Plater* current_plater()
{
if (wxTheApp == nullptr)
throw std::runtime_error("OrcaSlicer application is not initialized");
GUI::Plater* plater = GUI::wxGetApp().plater();
if (plater == nullptr)
throw std::runtime_error("Plater is not available");
return plater;
}
PresetBundle* current_preset_bundle()
{
if (wxTheApp == nullptr)
throw std::runtime_error("OrcaSlicer application is not initialized");
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
if (preset_bundle == nullptr)
throw std::runtime_error("Preset bundle is not available");
return preset_bundle;
}
py::object config_value_or_none(const DynamicPrintConfig& config, const std::string& key)
{
if (!config.has(key))
return py::none();
return py::cast(config.opt_serialize(key));
}
// Plugins receive 3D vectors as plain Python tuples (x, y, z) so the API stays
// Pythonic and free of an Eigen/numpy runtime dependency.
py::tuple vec3_to_tuple(const Vec3d& v)
{
return py::make_tuple(v.x(), v.y(), v.z());
}
// Build a BoundingBoxf3 from precomputed (float) triangle-mesh stats min/max.
BoundingBoxf3 bbox_from_stats(const TriangleMeshStats& stats)
{
if (stats.number_of_facets == 0)
return BoundingBoxf3();
return BoundingBoxf3(stats.min.cast<double>(), stats.max.cast<double>());
}
// --- Mesh geometry helpers -------------------------------------------------
// Zero-copy export of its.vertices / its.indices relies on these Eigen
// row-vectors being tightly packed (no padding between the 3 components).
static_assert(sizeof(stl_vertex) == 3 * sizeof(float),
"stl_vertex must be a packed float[3] for zero-copy numpy export");
static_assert(sizeof(stl_triangle_vertex_indices) == 3 * sizeof(std::int32_t),
"triangle index must be a packed int32[3] for zero-copy numpy export");
// Immutable snapshot of a ModelVolume's mesh. Holding a strong reference to the
// const mesh keeps any zero-copy numpy views valid even if the volume's mesh is
// later replaced on the main thread.
struct HostTriangleMesh
{
std::shared_ptr<const TriangleMesh> mesh;
const indexed_triangle_set& its() const { return mesh->its; }
};
// Run a builder that constructs numpy objects, translating the "numpy missing"
// ImportError into an actionable message (plugins must declare numpy as a dep).
template<typename Builder>
py::object with_numpy(Builder&& build)
{
try {
return std::forward<Builder>(build)();
} catch (py::error_already_set& err) {
if (err.matches(PyExc_ImportError))
throw py::import_error("numpy is required to access mesh arrays/matrices; "
"add dependencies = [\"numpy\"] to your plugin metadata");
throw;
}
}
// Read-only, zero-copy (rows, 3) numpy view over a packed T[rows][3] buffer.
// The array owns a capsule that pins `mesh` alive for the view's lifetime.
template<typename T>
py::array make_readonly_rows3(const std::shared_ptr<const TriangleMesh>& mesh,
const T* data, py::ssize_t rows)
{
if (rows == 0 || data == nullptr)
return py::array_t<T>(std::vector<py::ssize_t>{0, 3});
auto* owner = new std::shared_ptr<const TriangleMesh>(mesh);
py::capsule base(owner, [](void* p) {
delete reinterpret_cast<std::shared_ptr<const TriangleMesh>*>(p);
});
py::array_t<T> array(
{ rows, py::ssize_t(3) },
{ py::ssize_t(3 * sizeof(T)), py::ssize_t(sizeof(T)) },
data,
base);
// A capsule-based array is writable by default in pybind11; the underlying
// mesh is const, so force the view read-only.
array.attr("setflags")(py::arg("write") = false);
return array;
}
// 4x4 row-major float64 copy of an affine transform. Eigen stores column-major,
// so fill element-wise to produce correct C-order data.
py::object mat4_to_numpy(const Transform3d& transform)
{
return with_numpy([&] {
py::array_t<double> array({ py::ssize_t(4), py::ssize_t(4) });
auto view = array.mutable_unchecked<2>();
const auto& matrix = transform.matrix();
for (int i = 0; i < 4; ++i)
for (int j = 0; j < 4; ++j)
view(i, j) = matrix(i, j);
return py::object(std::move(array));
});
}
py::list current_filament_presets(PresetBundle& bundle)
{
py::list presets;
for (const std::string& preset_name : bundle.filament_presets) {
Preset* preset = bundle.filaments.find_preset(preset_name);
if (preset == nullptr)
presets.append(py::none());
else
presets.append(py::cast(preset, py::return_value_policy::reference));
}
return presets;
}
PresetCollection& printer_presets(PresetBundle& bundle)
{
return static_cast<PresetCollection&>(bundle.printers);
}
} // namespace
void PluginHostApi::RegisterBindings(pybind11::module_& module)
{
auto host = module.def_submodule("host", "Host application API");
py::enum_<Preset::Type>(host, "PresetType")
.value("Invalid", Preset::TYPE_INVALID)
.value("Print", Preset::TYPE_PRINT)
.value("SlaPrint", Preset::TYPE_SLA_PRINT)
.value("Filament", Preset::TYPE_FILAMENT)
.value("SlaMaterial", Preset::TYPE_SLA_MATERIAL)
.value("Printer", Preset::TYPE_PRINTER)
.value("PhysicalPrinter", Preset::TYPE_PHYSICAL_PRINTER)
.value("Plate", Preset::TYPE_PLATE)
.value("Model", Preset::TYPE_MODEL);
py::class_<Preset, std::unique_ptr<Preset, py::nodelete>>(host, "Preset")
.def_readonly("type", &Preset::type)
.def_readonly("name", &Preset::name)
.def_readonly("alias", &Preset::alias)
.def_readonly("file", &Preset::file)
.def_readonly("is_default", &Preset::is_default)
.def_readonly("is_external", &Preset::is_external)
.def_readonly("is_system", &Preset::is_system)
.def_readonly("is_visible", &Preset::is_visible)
.def_readonly("is_dirty", &Preset::is_dirty)
.def_readonly("is_compatible", &Preset::is_compatible)
.def_readonly("is_project_embedded", &Preset::is_project_embedded)
.def_readonly("bundle_id", &Preset::bundle_id)
.def("is_user", &Preset::is_user)
.def("is_from_bundle", &Preset::is_from_bundle)
.def("label", &Preset::label, py::arg("no_alias") = false)
.def("config_keys", [](const Preset& preset) { return preset.config.keys(); })
.def("config_value", [](const Preset& preset, const std::string& key) {
return config_value_or_none(preset.config, key);
});
py::class_<PresetCollection, std::unique_ptr<PresetCollection, py::nodelete>>(host, "PresetCollection")
.def("size", &PresetCollection::size)
.def("get_selected_preset", [](PresetCollection& collection) -> Preset& {
return collection.get_selected_preset();
}, py::return_value_policy::reference_internal)
.def("selected_preset", [](PresetCollection& collection) -> Preset& {
return collection.get_selected_preset();
}, py::return_value_policy::reference_internal)
.def("get_selected_preset_name", &PresetCollection::get_selected_preset_name)
.def("selected_preset_name", &PresetCollection::get_selected_preset_name)
.def("get_edited_preset", [](PresetCollection& collection) -> Preset& {
return collection.get_edited_preset();
}, py::return_value_policy::reference_internal)
.def("edited_preset", [](PresetCollection& collection) -> Preset& {
return collection.get_edited_preset();
}, py::return_value_policy::reference_internal)
.def("preset", [](PresetCollection& collection, size_t index) -> Preset& {
if (index >= collection.size())
throw py::index_error("preset index out of range");
return collection.preset(index);
}, py::return_value_policy::reference_internal)
.def("find_preset", [](PresetCollection& collection, const std::string& name) -> Preset* {
return collection.find_preset(name);
}, py::return_value_policy::reference_internal)
.def("preset_names", [](const PresetCollection& collection) {
std::vector<std::string> names;
names.reserve(collection.get_presets().size());
for (const Preset& preset : collection.get_presets())
names.push_back(preset.name);
return names;
});
py::class_<PresetBundle, std::unique_ptr<PresetBundle, py::nodelete>>(host, "PresetBundle")
.def_property_readonly("prints", [](PresetBundle& bundle) -> PresetCollection& {
return bundle.prints;
}, py::return_value_policy::reference_internal)
.def_property_readonly("printers", &printer_presets, py::return_value_policy::reference_internal)
.def_property_readonly("filaments", [](PresetBundle& bundle) -> PresetCollection& {
return bundle.filaments;
}, py::return_value_policy::reference_internal)
.def_property_readonly("sla_prints", [](PresetBundle& bundle) -> PresetCollection& {
return bundle.sla_prints;
}, py::return_value_policy::reference_internal)
.def_property_readonly("sla_materials", [](PresetBundle& bundle) -> PresetCollection& {
return bundle.sla_materials;
}, py::return_value_policy::reference_internal)
.def("current_process_preset", [](PresetBundle& bundle) -> Preset& {
return bundle.prints.get_edited_preset();
}, py::return_value_policy::reference_internal)
.def("current_print_preset", [](PresetBundle& bundle) -> Preset& {
return bundle.prints.get_edited_preset();
}, py::return_value_policy::reference_internal)
.def("current_printer_preset", [](PresetBundle& bundle) -> Preset& {
return bundle.printers.get_edited_preset();
}, py::return_value_policy::reference_internal)
.def("current_filament_preset_names", [](PresetBundle& bundle) {
return bundle.filament_presets;
})
.def("current_filament_presets", &current_filament_presets)
.def("full_config_keys", [](const PresetBundle& bundle) {
return bundle.full_config().keys();
})
.def("full_config_value", [](const PresetBundle& bundle, const std::string& key) {
return config_value_or_none(bundle.full_config(), key);
});
// Axis-aligned bounding box, returned by value (a copy) so its lifetime is
// independent of the model object it was computed from. Coordinates are in mm.
py::class_<BoundingBoxf3>(host, "BoundingBox", "Axis-aligned bounding box in millimetres")
.def_property_readonly("defined", [](const BoundingBoxf3& bb) { return bb.defined; })
.def_property_readonly("min", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.min); })
.def_property_readonly("max", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.max); })
.def_property_readonly("size", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.size()); })
.def_property_readonly("center", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.center()); })
.def_property_readonly("radius", [](const BoundingBoxf3& bb) { return bb.radius(); });
py::class_<HostTriangleMesh>(host, "TriangleMesh",
"Immutable snapshot of a ModelVolume's mesh in local (untransformed) coordinates, mm.")
.def("vertex_count", [](const HostTriangleMesh& mesh) { return mesh.its().vertices.size(); })
.def("triangle_count", [](const HostTriangleMesh& mesh) { return mesh.its().indices.size(); })
.def("facets_count", [](const HostTriangleMesh& mesh) { return mesh.its().indices.size(); })
.def("is_empty", [](const HostTriangleMesh& mesh) { return mesh.its().indices.empty(); })
// Read-only, zero-copy (N, 3) float32 view of vertex positions. Requires numpy.
.def("vertices", [](const HostTriangleMesh& mesh) {
return with_numpy([&] {
const indexed_triangle_set& its = mesh.its();
return make_readonly_rows3<float>(
mesh.mesh,
its.vertices.empty() ? nullptr : its.vertices.front().data(),
static_cast<py::ssize_t>(its.vertices.size()));
});
}, "Read-only zero-copy (N, 3) float32 ndarray of vertex positions (local mm). Requires numpy.")
// Read-only, zero-copy (M, 3) int32 view of triangle vertex indices. Requires numpy.
.def("triangles", [](const HostTriangleMesh& mesh) {
return with_numpy([&] {
const indexed_triangle_set& its = mesh.its();
return make_readonly_rows3<std::int32_t>(
mesh.mesh,
its.indices.empty() ? nullptr : its.indices.front().data(),
static_cast<py::ssize_t>(its.indices.size()));
});
}, "Read-only zero-copy (M, 3) int32 ndarray of triangle vertex indices. Requires numpy.")
// One normalized normal per triangle as an (M, 3) float32 copy. Requires numpy.
.def("face_normals", [](const HostTriangleMesh& mesh) {
return with_numpy([&] {
std::vector<Vec3f> normals = its_face_normals(mesh.its());
py::array_t<float> array({ static_cast<py::ssize_t>(normals.size()), py::ssize_t(3) });
if (!normals.empty()) {
auto view = array.mutable_unchecked<2>();
for (size_t i = 0; i < normals.size(); ++i) {
view(i, 0) = normals[i].x();
view(i, 1) = normals[i].y();
view(i, 2) = normals[i].z();
}
}
return py::object(std::move(array));
});
}, "Per-triangle normalized normals as an (M, 3) float32 ndarray (copy). Requires numpy.")
// numpy-free element access, bounds-checked.
.def("vertex", [](const HostTriangleMesh& mesh, size_t index) {
const std::vector<stl_vertex>& vertices = mesh.its().vertices;
if (index >= vertices.size())
throw py::index_error("vertex index out of range");
const stl_vertex& vertex = vertices[index];
return py::make_tuple(vertex.x(), vertex.y(), vertex.z());
})
.def("triangle", [](const HostTriangleMesh& mesh, size_t index) {
const std::vector<stl_triangle_vertex_indices>& indices = mesh.its().indices;
if (index >= indices.size())
throw py::index_error("triangle index out of range");
const stl_triangle_vertex_indices& triangle = indices[index];
return py::make_tuple(triangle[0], triangle[1], triangle[2]);
})
.def("volume", [](const HostTriangleMesh& mesh) { return mesh.mesh->stats().volume; })
.def("bounding_box", [](const HostTriangleMesh& mesh) { return bbox_from_stats(mesh.mesh->stats()); })
.def("is_manifold", [](const HostTriangleMesh& mesh) { return mesh.mesh->stats().manifold(); });
py::enum_<ModelVolumeType>(host, "ModelVolumeType")
.value("Invalid", ModelVolumeType::INVALID)
.value("ModelPart", ModelVolumeType::MODEL_PART)
.value("NegativeVolume", ModelVolumeType::NEGATIVE_VOLUME)
.value("ParameterModifier", ModelVolumeType::PARAMETER_MODIFIER)
.value("SupportBlocker", ModelVolumeType::SUPPORT_BLOCKER)
.value("SupportEnforcer", ModelVolumeType::SUPPORT_ENFORCER);
py::class_<ModelVolume, std::unique_ptr<ModelVolume, py::nodelete>>(host, "ModelVolume")
.def("id", [](const ModelVolume& volume) { return volume.id().id; })
.def_readonly("name", &ModelVolume::name)
.def("type", &ModelVolume::type)
.def("is_model_part", &ModelVolume::is_model_part)
.def("is_modifier", &ModelVolume::is_modifier)
.def("is_negative_volume", &ModelVolume::is_negative_volume)
.def("is_support_enforcer", &ModelVolume::is_support_enforcer)
.def("is_support_blocker", &ModelVolume::is_support_blocker)
.def("is_support_modifier", &ModelVolume::is_support_modifier)
// Extruder ID is 1-based for FFF, -1 for SLA or support volumes.
.def("extruder_id", &ModelVolume::extruder_id)
.def("offset", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_offset()); })
.def("rotation", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_rotation()); })
.def("scaling_factor", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_scaling_factor()); })
.def("mirror", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_mirror()); })
// 4x4 float64 affine matrix mapping this volume into its parent object frame. Requires numpy.
.def("matrix", [](const ModelVolume& volume) { return mat4_to_numpy(volume.get_matrix()); },
"Volume-to-object 4x4 float64 affine matrix (copy). Requires numpy.")
.def("facets_count", [](const ModelVolume& volume) { return volume.mesh().facets_count(); })
// Raw (untransformed) mesh volume in mm^3; -1 if it was never computed.
.def("volume", [](const ModelVolume& volume) { return volume.mesh().stats().volume; })
// Bounding box of the raw (untransformed) mesh, in the volume's local frame.
.def("bounding_box", [](const ModelVolume& volume) { return bbox_from_stats(volume.mesh().stats()); })
.def("is_manifold", [](const ModelVolume& volume) { return volume.mesh().stats().manifold(); })
// Full mesh geometry (vertices/triangles) as an immutable snapshot.
.def("mesh", [](const ModelVolume& volume) {
return HostTriangleMesh{ volume.get_mesh_shared_ptr() };
}, "Return the volume's TriangleMesh (local coordinates) for vertex/triangle access.")
.def("mesh_errors_count", [](const ModelVolume& volume) { return volume.get_repaired_errors_count(); })
.def("is_fdm_support_painted", &ModelVolume::is_fdm_support_painted)
.def("is_seam_painted", &ModelVolume::is_seam_painted)
.def("is_mm_painted", &ModelVolume::is_mm_painted)
.def("is_fuzzy_skin_painted", &ModelVolume::is_fuzzy_skin_painted)
.def("config_keys", [](const ModelVolume& volume) { return volume.config.keys(); })
.def("config_value", [](const ModelVolume& volume, const std::string& key) {
return config_value_or_none(volume.config.get(), key);
});
py::class_<ModelInstance, std::unique_ptr<ModelInstance, py::nodelete>>(host, "ModelInstance")
.def("id", [](const ModelInstance& instance) { return instance.id().id; })
.def_readonly("printable", &ModelInstance::printable)
// True only if the object is printable, this instance is printable and it
// currently sits fully inside the print volume (set during slicing).
.def("is_printable", &ModelInstance::is_printable)
.def("offset", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_offset()); })
.def("rotation", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_rotation()); })
.def("scaling_factor", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_scaling_factor()); })
.def("mirror", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_mirror()); })
// 4x4 float64 affine matrix mapping the object into world space. Requires numpy.
// World vertices = instance.matrix() @ volume.matrix() applied to mesh vertices.
.def("matrix", [](const ModelInstance& instance) { return mat4_to_numpy(instance.get_matrix()); },
"Object-to-world 4x4 float64 affine matrix (copy). Requires numpy.")
.def("is_left_handed", &ModelInstance::is_left_handed)
// World-space bounding box of this instance.
.def("bounding_box", [](ModelInstance& instance) {
const ModelObject* object = instance.get_object();
if (object == nullptr)
return BoundingBoxf3();
return object->instance_bounding_box(instance);
});
py::class_<ModelObject, std::unique_ptr<ModelObject, py::nodelete>>(host, "ModelObject")
.def("id", [](const ModelObject& object) { return object.id().id; })
.def_readonly("name", &ModelObject::name)
.def_readonly("module_name", &ModelObject::module_name)
.def_readonly("input_file", &ModelObject::input_file)
.def_readonly("printable", &ModelObject::printable)
.def("instance_count", [](const ModelObject& object) {
return object.instances.size();
})
.def("volume_count", [](const ModelObject& object) {
return object.volumes.size();
})
.def("instances", [](ModelObject& object) {
py::list instances;
for (ModelInstance* instance : object.instances)
instances.append(py::cast(instance, py::return_value_policy::reference));
return instances;
})
.def("instance", [](ModelObject& object, size_t index) -> ModelInstance* {
if (index >= object.instances.size())
throw py::index_error("instance index out of range");
return object.instances[index];
}, py::return_value_policy::reference_internal)
.def("volumes", [](ModelObject& object) {
py::list volumes;
for (ModelVolume* volume : object.volumes)
volumes.append(py::cast(volume, py::return_value_policy::reference));
return volumes;
})
.def("volume", [](ModelObject& object, size_t index) -> ModelVolume* {
if (index >= object.volumes.size())
throw py::index_error("volume index out of range");
return object.volumes[index];
}, py::return_value_policy::reference_internal)
// World-space bounding box over all instances of this object.
.def("bounding_box", [](const ModelObject& object) { return object.bounding_box_exact(); })
// Bounding box of the object's raw (untransformed) part meshes — its intrinsic size.
.def("raw_mesh_bounding_box", [](const ModelObject& object) { return object.raw_mesh_bounding_box(); })
.def("min_z", &ModelObject::min_z)
.def("max_z", &ModelObject::max_z)
.def("facets_count", [](const ModelObject& object) { return object.facets_count(); })
.def("parts_count", [](const ModelObject& object) { return object.parts_count(); })
.def("materials_count", [](const ModelObject& object) { return object.materials_count(); })
.def("mesh_errors_count", [](const ModelObject& object) { return object.get_repaired_errors_count(); })
.def("is_multiparts", &ModelObject::is_multiparts)
.def("is_cut", &ModelObject::is_cut)
.def("has_custom_layering", &ModelObject::has_custom_layering)
.def("is_fdm_support_painted", &ModelObject::is_fdm_support_painted)
.def("is_seam_painted", &ModelObject::is_seam_painted)
.def("is_mm_painted", &ModelObject::is_mm_painted)
.def("is_fuzzy_skin_painted", &ModelObject::is_fuzzy_skin_painted)
.def("config_keys", [](const ModelObject& object) {
return object.config.keys();
})
.def("config_value", [](const ModelObject& object, const std::string& key) {
return config_value_or_none(object.config.get(), key);
});
py::class_<Model, std::unique_ptr<Model, py::nodelete>>(host, "Model")
.def("id", [](const Model& model) { return model.id().id; })
.def("object_count", [](const Model& model) {
return model.objects.size();
})
.def("object", [](Model& model, size_t index) -> ModelObject* {
if (index >= model.objects.size())
throw py::index_error("model object index out of range");
return model.objects[index];
}, py::return_value_policy::reference_internal)
.def("objects", [](Model& model) {
py::list objects;
for (ModelObject* object : model.objects)
objects.append(py::cast(object, py::return_value_policy::reference));
return objects;
})
// World-space bounding box of the whole model. bounding_box() is exact;
// bounding_box_approx() is faster and cached.
.def("bounding_box", [](const Model& model) { return model.bounding_box_exact(); })
.def("bounding_box_approx", [](const Model& model) { return model.bounding_box_approx(); })
.def("max_z", &Model::max_z)
.def("material_count", [](const Model& model) { return model.materials.size(); })
.def("is_fdm_support_painted", &Model::is_fdm_support_painted)
.def("is_seam_painted", &Model::is_seam_painted)
.def("is_mm_painted", &Model::is_mm_painted)
.def("is_fuzzy_skin_painted", &Model::is_fuzzy_skin_painted)
.def("current_plate_index", [](const Model& model) { return model.curr_plate_index; })
.def("designer", [](const Model& model) {
return model.design_info ? model.design_info->Designer : std::string();
})
.def("design_id", [](const Model& model) { return model.stl_design_id; });
py::class_<GUI::Plater, std::unique_ptr<GUI::Plater, py::nodelete>>(host, "Plater")
.def("model", static_cast<Model& (GUI::Plater::*)()>(&GUI::Plater::model), py::return_value_policy::reference_internal)
.def("is_project_dirty", &GUI::Plater::is_project_dirty)
.def("is_presets_dirty", &GUI::Plater::is_presets_dirty)
.def("inside_snapshot_capture", &GUI::Plater::inside_snapshot_capture);
host.def("plater", &current_plater, py::return_value_policy::reference);
host.def("model", []() -> Model& {
return current_plater()->model();
}, py::return_value_policy::reference);
host.def("preset_bundle", &current_preset_bundle, py::return_value_policy::reference);
// UI: native dialogs and interactive HTML windows for plugins.
PluginHostUi::RegisterBindings(host);
}
} // namespace Slic3r
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <pybind11/pybind11.h>
namespace Slic3r {
class PluginHostApi
{
public:
static void RegisterBindings(pybind11::module_& module);
};
} // namespace Slic3r
+562
View File
@@ -0,0 +1,562 @@
#include "PluginHostUi.hpp"
#include "PluginAuditManager.hpp"
#include "PythonInterpreter.hpp" // PythonGILState
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/MainFrame.hpp>
#include <slic3r/GUI/MsgDialog.hpp>
#include <slic3r/GUI/PluginProgressDialog.hpp>
#include <slic3r/GUI/PluginWebDialog.hpp>
#include <nlohmann/json.hpp>
#include <pybind11/pybind11.h>
#include <boost/log/trivial.hpp>
#include <wx/app.h>
#include <wx/defs.h>
#include <wx/window.h>
#include <cstdint>
#include <future>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <unordered_map>
#include <vector>
namespace py = pybind11;
using json = nlohmann::json;
namespace Slic3r {
namespace {
// --------------------------------------------------------------------------
// JSON <-> Python conversion (caller must hold the GIL).
// --------------------------------------------------------------------------
py::object json_to_py(const json& j)
{
switch (j.type()) {
case json::value_t::null: return py::none();
case json::value_t::boolean: return py::bool_(j.get<bool>());
case json::value_t::number_integer: return py::int_(j.get<std::int64_t>());
case json::value_t::number_unsigned: return py::int_(j.get<std::uint64_t>());
case json::value_t::number_float: return py::float_(j.get<double>());
case json::value_t::string: return py::str(j.get<std::string>());
case json::value_t::array: {
py::list lst;
for (const auto& e : j)
lst.append(json_to_py(e));
return lst;
}
case json::value_t::object: {
py::dict d;
for (auto it = j.begin(); it != j.end(); ++it)
d[py::str(it.key())] = json_to_py(it.value());
return d;
}
default: return py::none();
}
}
json py_to_json(const py::handle& o)
{
if (o.is_none())
return json(nullptr);
if (py::isinstance<py::bool_>(o)) // bool before int (bool subclasses int in Python)
return o.cast<bool>();
if (py::isinstance<py::int_>(o))
return o.cast<std::int64_t>();
if (py::isinstance<py::float_>(o))
return o.cast<double>();
if (py::isinstance<py::str>(o))
return o.cast<std::string>();
if (py::isinstance<py::bytes>(o))
return o.cast<std::string>();
if (py::isinstance<py::dict>(o)) {
json obj = json::object();
for (auto item : py::reinterpret_borrow<py::dict>(o))
obj[py::str(item.first).cast<std::string>()] = py_to_json(item.second);
return obj;
}
if (py::isinstance<py::list>(o) || py::isinstance<py::tuple>(o)) {
json arr = json::array();
for (auto e : o)
arr.push_back(py_to_json(e));
return arr;
}
return py::str(o).cast<std::string>(); // fallback: str()
}
// --------------------------------------------------------------------------
// GIL-safe holder for a Python callable. A std::function that captured a bare
// py::object could be destroyed on the main thread without the GIL (a dialog
// teardown), which would Py_DECREF unsafely. Wrapping the callable here means
// the GIL is acquired exactly when the last reference is released, on any thread.
// --------------------------------------------------------------------------
struct GilSafeCallable
{
py::object fn;
explicit GilSafeCallable(py::object f) : fn(std::move(f)) {}
~GilSafeCallable()
{
if (fn) {
PythonGILState gil;
fn = py::object();
}
}
};
using CallablePtr = std::shared_ptr<GilSafeCallable>;
CallablePtr make_holder(py::object obj)
{
if (!obj || obj.is_none())
return nullptr;
return std::make_shared<GilSafeCallable>(std::move(obj));
}
// Adapt a Python callable to a GUI message handler that acquires the GIL and
// swallows/logs exceptions (a raising handler must not escape into wx events).
GUI::PluginWebDialog::MessageHandler make_message_adapter(py::object on_message)
{
CallablePtr holder = make_holder(std::move(on_message));
if (!holder)
return nullptr;
return [holder](const json& data) {
PythonGILState gil;
try {
holder->fn(json_to_py(data));
} catch (py::error_already_set& e) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_message handler raised: " << e.what();
PyErr_Clear();
}
};
}
// --------------------------------------------------------------------------
// Registry of live plugin UI resources. Keyed by an opaque id; tracks the
// owning plugin so all of a plugin's UI can be torn down on unload.
// --------------------------------------------------------------------------
class UiRegistry
{
public:
static UiRegistry& instance()
{
static UiRegistry r;
return r;
}
int reserve_id()
{
std::lock_guard<std::mutex> lk(m_mtx);
return m_next_id++;
}
void bind(int id, wxWindow* window, const std::string& plugin_key)
{
std::lock_guard<std::mutex> lk(m_mtx);
m_resources[id] = window;
m_owners[id] = plugin_key;
}
void remove(int id)
{
std::lock_guard<std::mutex> lk(m_mtx);
m_resources.erase(id);
m_owners.erase(id);
}
template<typename T>
T* get_as(int id)
{
std::lock_guard<std::mutex> lk(m_mtx);
auto it = m_resources.find(id);
if (it == m_resources.end())
return nullptr;
return dynamic_cast<T*>(it->second);
}
bool is_open(int id)
{
std::lock_guard<std::mutex> lk(m_mtx);
return m_resources.count(id) > 0; // presence only; no pointer deref -> thread-safe
}
std::vector<wxWindow*> take_for_plugin(const std::string& plugin_key)
{
std::lock_guard<std::mutex> lk(m_mtx);
std::vector<wxWindow*> out;
for (auto it = m_owners.begin(); it != m_owners.end();) {
if (it->second == plugin_key) {
auto rit = m_resources.find(it->first);
if (rit != m_resources.end()) {
out.push_back(rit->second);
m_resources.erase(rit);
}
it = m_owners.erase(it);
} else {
++it;
}
}
return out;
}
private:
std::mutex m_mtx;
std::unordered_map<int, wxWindow*> m_resources;
std::unordered_map<int, std::string> m_owners;
int m_next_id{1};
};
// --------------------------------------------------------------------------
// Run a (pure C++/wx) callable on the main/UI thread, blocking the caller until
// it completes, with the GIL released across the wait. If already on the main
// thread, run inline (also with the GIL released so other Python threads run).
// --------------------------------------------------------------------------
template<typename Fn>
auto run_on_ui_blocking(Fn&& fn) -> std::invoke_result_t<Fn&>
{
using R = std::invoke_result_t<Fn&>;
if (wxTheApp == nullptr)
throw std::runtime_error("OrcaSlicer application is not initialized");
if (wxIsMainThread()) {
py::gil_scoped_release nogil;
return fn();
}
std::promise<R> prom;
std::future<R> fut = prom.get_future();
py::gil_scoped_release nogil;
GUI::wxGetApp().CallAfter([&prom, &fn]() {
try {
if constexpr (std::is_void_v<R>) {
fn();
prom.set_value();
} else {
prom.set_value(fn());
}
} catch (...) {
prom.set_exception(std::current_exception());
}
});
return fut.get();
}
wxWindow* ui_parent()
{
return wxTheApp == nullptr ? nullptr : dynamic_cast<wxWindow*>(GUI::wxGetApp().mainframe);
}
// --------------------------------------------------------------------------
// orca.host.ui.message
// --------------------------------------------------------------------------
long message_style(const std::string& buttons, const std::string& icon)
{
long style = wxOK;
if (buttons == "ok_cancel")
style = wxOK | wxCANCEL;
else if (buttons == "yes_no")
style = wxYES_NO;
else if (buttons == "yes_no_cancel")
style = wxYES_NO | wxCANCEL;
if (icon == "warning")
style |= wxICON_WARNING;
else if (icon == "error")
style |= wxICON_ERROR;
else if (icon == "question")
style |= wxICON_QUESTION;
else
style |= wxICON_INFORMATION;
return style;
}
std::string button_to_string(int rc)
{
switch (rc) {
case wxID_OK: return "ok";
case wxID_YES: return "yes";
case wxID_NO: return "no";
default: return "cancel";
}
}
std::string ui_message(const std::string& text, const std::string& title,
const std::string& buttons, const std::string& icon)
{
const long style = message_style(buttons, icon);
return run_on_ui_blocking([&]() -> std::string {
GUI::MessageDialog dlg(nullptr, wxString::FromUTF8(text), wxString::FromUTF8(title), style);
return button_to_string(dlg.ShowModal());
});
}
// --------------------------------------------------------------------------
// orca.host.ui.show_dialog (modal)
// --------------------------------------------------------------------------
py::object ui_show_dialog(const std::string& html, const std::string& title,
int width, int height, py::object on_message)
{
auto handler = make_message_adapter(std::move(on_message));
const int w = width > 0 ? width : 820;
const int h = height > 0 ? height : 600;
std::optional<json> result = run_on_ui_blocking([&]() -> std::optional<json> {
return GUI::PluginWebDialog::show_modal_dialog(ui_parent(), wxString::FromUTF8(title), html, wxSize(w, h),
std::move(handler));
});
if (!result.has_value())
return py::none();
return json_to_py(*result); // GIL held in the binding body
}
// --------------------------------------------------------------------------
// orca.host.ui.create_window (non-modal) + UiWindow handle
// --------------------------------------------------------------------------
struct UiWindowHandle
{
int id{0};
};
struct UiProgressHandle
{
int id{0};
};
py::object ui_create_window(const std::string& html, const std::string& title, int width, int height,
py::object on_message, py::object on_close)
{
auto msg_adapter = make_message_adapter(std::move(on_message));
CallablePtr close_holder = make_holder(std::move(on_close));
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
const int w = width > 0 ? width : 820;
const int h = height > 0 ? height : 600;
const int id = run_on_ui_blocking([&]() -> int {
const int new_id = UiRegistry::instance().reserve_id();
// Plugin's on_close: fired only on a user/JS-initiated close (not forced
// teardown), while the dialog is alive. Empty if the plugin passed None.
GUI::PluginWebDialog::CloseHandler on_close;
if (close_holder) {
on_close = [close_holder]() {
PythonGILState gil;
try {
close_holder->fn();
} catch (py::error_already_set& e) {
BOOST_LOG_TRIVIAL(error) << "orca.host.ui on_close handler raised: " << e.what();
PyErr_Clear();
}
};
}
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
auto* dlg = GUI::PluginWebDialog::create_modeless_dialog(ui_parent(), wxString::FromUTF8(title), html,
wxSize(w, h), std::move(msg_adapter),
std::move(on_close), std::move(on_destroyed));
UiRegistry::instance().bind(new_id, dlg, plugin_key);
GUI::PluginWebDialog::show_modeless_dialog(dlg);
return new_id;
});
return py::cast(UiWindowHandle{id});
}
void handle_post(int id, py::object data)
{
if (wxTheApp == nullptr)
return;
json j = py_to_json(data); // GIL held (binding body)
GUI::wxGetApp().CallAfter([id, j = std::move(j)]() {
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
GUI::PluginWebDialog::post_message(d, j);
});
}
void handle_close(int id)
{
if (wxTheApp == nullptr)
return;
GUI::wxGetApp().CallAfter([id]() {
auto* d = UiRegistry::instance().get_as<GUI::PluginWebDialog>(id);
GUI::PluginWebDialog::request_close(d);
});
}
UiProgressHandle ui_create_progress_dialog(const std::string& title, const std::string& message, int maximum, int style)
{
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
const int max_value = maximum > 0 ? maximum : 100;
return run_on_ui_blocking([&]() -> UiProgressHandle {
const int new_id = UiRegistry::instance().reserve_id();
// Registry cleanup: GIL-free, runs from the dialog destructor on every path.
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
auto* dlg = GUI::PluginProgressDialog::create_dialog(ui_parent(), wxString::FromUTF8(title),
wxString::FromUTF8(message), max_value, style,
std::move(on_destroyed));
UiRegistry::instance().bind(new_id, dlg, plugin_key);
return UiProgressHandle{new_id};
});
}
UiProgressHandle* new_progress_dialog(const std::string& title, const std::string& message, int maximum, int style)
{
return new UiProgressHandle(ui_create_progress_dialog(title, message, maximum, style));
}
bool progress_is_open(int id)
{
return UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id) != nullptr;
}
bool progress_pulse(int id, const std::string& message)
{
return run_on_ui_blocking([&]() {
auto* d = UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id);
return GUI::PluginProgressDialog::pulse(d, wxString::FromUTF8(message));
});
}
bool progress_update(int id, int value, const std::string& message)
{
return run_on_ui_blocking([&]() {
auto* d = UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id);
return GUI::PluginProgressDialog::update(d, value, wxString::FromUTF8(message));
});
}
void progress_start_pulse(int id, int interval_ms, const std::string& message)
{
run_on_ui_blocking([&]() {
auto* d = UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id);
GUI::PluginProgressDialog::start_pulse(d, interval_ms, wxString::FromUTF8(message));
});
}
void progress_stop_pulse(int id)
{
run_on_ui_blocking([&]() {
auto* d = UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id);
GUI::PluginProgressDialog::stop_pulse(d);
});
}
void progress_close(int id)
{
run_on_ui_blocking([&]() {
auto* d = UiRegistry::instance().get_as<GUI::PluginProgressDialog>(id);
GUI::PluginProgressDialog::request_close(d);
});
}
} // namespace
void PluginHostUi::RegisterBindings(pybind11::module_& host)
{
auto ui = host.def_submodule(
"ui",
"Host UI: native dialogs and interactive HTML windows. Calls run on the main/UI "
"thread (marshaled from the plugin thread). See the plugin docs for the window.orca bridge.");
ui.def("message", &ui_message, py::arg("text"), py::arg("title") = "OrcaSlicer", py::arg("buttons") = "ok",
py::arg("icon") = "info",
"Show a native modal message box; returns the clicked button id "
"(\"ok\"/\"cancel\"/\"yes\"/\"no\"). buttons: \"ok\"|\"ok_cancel\"|\"yes_no\"|\"yes_no_cancel\"; "
"icon: \"info\"|\"warning\"|\"error\"|\"question\".");
ui.def("show_dialog", &ui_show_dialog, py::arg("html"), py::arg("title") = "OrcaSlicer", py::arg("width") = 820,
py::arg("height") = 600, py::arg("on_message") = py::none(),
"Show a modal dialog rendering the given raw HTML. The page talks to the plugin via "
"window.orca (postMessage/onMessage/submit/close). Blocks until closed; returns the "
"orca.submit() payload as a dict, or None.");
ui.attr("PD_APP_MODAL") = py::int_(wxPD_APP_MODAL);
ui.attr("PD_AUTO_HIDE") = py::int_(wxPD_AUTO_HIDE);
ui.attr("PD_CAN_ABORT") = py::int_(wxPD_CAN_ABORT);
ui.attr("PD_CAN_SKIP") = py::int_(wxPD_CAN_SKIP);
ui.attr("PD_ELAPSED_TIME") = py::int_(wxPD_ELAPSED_TIME);
ui.attr("PD_ESTIMATED_TIME") = py::int_(wxPD_ESTIMATED_TIME);
ui.attr("PD_REMAINING_TIME") = py::int_(wxPD_REMAINING_TIME);
py::class_<UiWindowHandle>(ui, "UiWindow", "Handle to a non-modal plugin window created by create_window().")
.def_property_readonly("id", [](const UiWindowHandle& h) { return h.id; })
.def(
"post", [](const UiWindowHandle& h, py::object data) { handle_post(h.id, std::move(data)); },
py::arg("data"), "Send a payload to the page (delivered to window.orca.onMessage handlers).")
.def(
"close", [](const UiWindowHandle& h) { handle_close(h.id); }, "Close the window (fires on_close).")
.def(
"is_open", [](const UiWindowHandle& h) { return UiRegistry::instance().is_open(h.id); },
"Return True while the window is open.");
ui.def("create_window", &ui_create_window, py::arg("html"), py::arg("title") = "OrcaSlicer", py::arg("width") = 820,
py::arg("height") = 600, py::arg("on_message") = py::none(), py::arg("on_close") = py::none(),
"Open a non-modal, persistent HTML window and return a UiWindow. on_message(data) is called on "
"the UI thread when the page posts; offload heavy work to a thread and push results back with "
"window.post().");
py::class_<UiProgressHandle>(ui, "ProgressDialog", "Handle to a native progress dialog.")
.def(py::init(&new_progress_dialog), py::arg("title"), py::arg("message"), py::arg("maximum") = 100,
py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
.def_property_readonly("id", [](const UiProgressHandle& h) { return h.id; })
.def(
"pulse", [](const UiProgressHandle& h, const std::string& message) { return progress_pulse(h.id, message); },
py::arg("message") = "", "Pulse the dialog gauge; returns False if the dialog is closed or cancelled.")
.def(
"update",
[](const UiProgressHandle& h, int value, const std::string& message) {
return progress_update(h.id, value, message);
},
py::arg("value"), py::arg("message") = "",
"Set the dialog progress value; returns False if the dialog is closed or cancelled.")
.def(
"start_pulse",
[](const UiProgressHandle& h, int interval_ms, const std::string& message) {
progress_start_pulse(h.id, interval_ms, message);
},
py::arg("interval_ms") = 100, py::arg("message") = "", "Start periodic pulsing.")
.def(
"stop_pulse", [](const UiProgressHandle& h) { progress_stop_pulse(h.id); }, "Stop periodic pulsing.")
.def(
"close", [](const UiProgressHandle& h) { progress_close(h.id); }, "Close the progress dialog.")
.def(
"is_open", [](const UiProgressHandle& h) { return progress_is_open(h.id); },
"Return True while this progress dialog is registered.")
.def("__enter__", [](UiProgressHandle& h) -> UiProgressHandle& { return h; }, py::return_value_policy::reference_internal)
.def("__exit__", [](const UiProgressHandle& h, py::object, py::object, py::object) {
progress_close(h.id);
return false;
});
ui.def("create_progress_dialog", &ui_create_progress_dialog, py::arg("title"), py::arg("message"),
py::arg("maximum") = 100, py::arg("style") = wxPD_APP_MODAL | wxPD_AUTO_HIDE,
"Create a native progress dialog and return a ProgressDialog handle.");
}
void PluginHostUi::close_windows_for_plugin(const std::string& plugin_key)
{
if (wxTheApp == nullptr)
return;
auto teardown = [plugin_key]() {
// Destroy() bypasses wxEVT_CLOSE, so the plugin's on_close is not fired on
// forced teardown (intended); the resource destructor still cleans the registry.
for (auto* window : UiRegistry::instance().take_for_plugin(plugin_key)) {
if (window != nullptr)
window->Destroy();
}
};
if (wxIsMainThread())
teardown();
else
GUI::wxGetApp().CallAfter(teardown);
}
} // namespace Slic3r
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <pybind11/pybind11.h>
#include <string>
namespace Slic3r {
// Binds the `orca.host.ui` submodule: native message boxes, progress dialogs,
// and interactive HTML windows for plugins. All calls run on the main/UI thread
// (marshaled from the plugin worker thread) and the host owns every window.
class PluginHostUi
{
public:
static void RegisterBindings(pybind11::module_& host);
// Lifecycle hook: close and tear down every UI window owned by a plugin.
// Registered via PluginLoader::subscribe_on_unload_callback so UI windows
// are destroyed on plugin unload/reload and at app shutdown (before the
// Python interpreter is finalized). Matches PluginLifecycleCompleteFn.
static void close_windows_for_plugin(const std::string& plugin_key);
};
} // namespace Slic3r
File diff suppressed because it is too large. Load diff
+212
View File
@@ -0,0 +1,212 @@
#pragma once
#include "PluginDescriptor.hpp"
#include <boost/filesystem/path.hpp>
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <condition_variable>
#include <chrono>
#include <cstddef>
#include <pybind11/embed.h>
namespace Slic3r {
class PluginCatalog;
// A single capability materialized from a plugin package. The owning LoadedPlugin
// holds the descriptor and the Python module; capabilities only back-reference the
// package by plugin_key and cache their resolved name.
struct LoadedPluginCapability
{
std::shared_ptr<PluginCapabilityInterface> instance; // Materialized capability instance
std::string name; // Cached from instance->get_name() at load time
std::string plugin_key; // Owning package
PluginCapabilityType type = PluginCapabilityType::Unknown; // cached from instance->get_type() at load (GUI reads this without the GIL)
std::atomic<bool> enabled{true}; // logical enable/disable; disabled capabilities are skipped by consumers but stay loaded
};
struct PluginCapabilityIdentifier
{
PluginCapabilityType type = PluginCapabilityType::Unknown;
std::string name;
std::string plugin_key; // owning package — makes the identity globally unique
bool operator==(const PluginCapabilityIdentifier& o) const
{ return type == o.type && name == o.name && plugin_key == o.plugin_key; }
};
// A loaded plugin package: one .py/.whl file → one descriptor + one module + N capabilities.
struct LoadedPlugin
{
PluginDescriptor descriptor;
PyObject* module = nullptr; // Python module object, shared by all capabilities
std::vector<PluginCapabilityIdentifier> capabilities;
LoadedPlugin() = default;
LoadedPlugin(const LoadedPlugin&) = delete;
LoadedPlugin& operator=(const LoadedPlugin&) = delete;
// Move transfers module ownership; the moved-from package must not Py_DECREF it.
LoadedPlugin(LoadedPlugin&& other) noexcept
: descriptor(std::move(other.descriptor)), module(other.module), capabilities(std::move(other.capabilities))
{ other.module = nullptr; }
LoadedPlugin& operator=(LoadedPlugin&& other) noexcept = delete;
~LoadedPlugin();
};
} // namespace Slic3r
template<> struct std::hash<Slic3r::PluginCapabilityIdentifier>
{
std::size_t operator()(const Slic3r::PluginCapabilityIdentifier& id) const noexcept
{
std::size_t h = std::hash<std::size_t>{}(static_cast<std::size_t>(id.type));
auto mix = [&h](std::size_t v) { h ^= v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2); };
mix(std::hash<std::string>{}(id.name));
mix(std::hash<std::string>{}(id.plugin_key));
return h;
}
};
namespace Slic3r {
class PluginLoader
{
public:
enum CallbackType {
Load,
Unload,
};
using PluginLoadInProgress = std::unordered_set<std::string>;
using PluginLoadErrors = std::unordered_map<std::string, std::string>;
using PluginLifecycleCompleteFn = std::function<void(const std::string&)>;
using PluginChangedCallbacks = std::unordered_map<PluginCapabilityType, std::vector<std::function<void(const std::vector<PluginDescriptor>&)>>>;
bool is_idle_and_empty() const;
bool is_plugin_loaded(const std::string& plugin_key) const;
bool is_plugin_load_in_progress(const std::string& plugin_key) const;
void wait_for_all_plugin_loads() const;
bool wait_for_all_plugin_loads(std::chrono::milliseconds timeout) const; // bounded; true if all finished, false on timeout
bool wait_for_plugin_load(const std::string& plugin_key,
std::chrono::milliseconds timeout,
std::string& error) const;
std::vector<PluginDescriptor> get_all_loaded_plugin_descriptors() const;
// Package descriptor accessor; returns nullptr when the package is not loaded.
std::vector<std::shared_ptr<LoadedPluginCapability>> get_plugin_capabilities_by_type(const std::string& plugin_type) const;
std::vector<std::shared_ptr<LoadedPluginCapability>> get_plugin_capabilities_by_type(PluginCapabilityType type) const;
std::vector<std::shared_ptr<LoadedPluginCapability>> get_plugin_capabilities_by_type(
const std::string& plugin_key, PluginCapabilityType type) const;
// Resolve a capability by its owning package + type + name. plugin_key is matched by equality.
std::shared_ptr<LoadedPluginCapability> get_plugin_capability_by_name(
const std::string& plugin_key, PluginCapabilityType type, const std::string& name) const;
std::shared_ptr<LoadedPluginCapability> try_get_plugin_capability_by_name_and_type(const std::string& capability_name, PluginCapabilityType type) const;
std::shared_ptr<LoadedPluginCapability> get_plugin_capability_by_name(const PluginCapabilityIdentifier& identifier) const;
std::vector<std::shared_ptr<LoadedPluginCapability>> get_loaded_plugin_capabilities(const std::string& plugin_key) const;
std::string get_plugin_load_error(const std::string& plugin_key) const;
bool cancel_plugin_load(const std::string& plugin_key);
bool cancel_plugin_unload(const std::string& plugin_key);
bool install_packages(const std::vector<std::string>& pkgs, std::string& error) const;
void unload_all_plugins();
bool unload_plugin(const std::string& plugin_key,
PluginCapabilityType type);
bool unload_plugin(const std::string& plugin_key);
void load_plugin(PluginCatalog& catalog,
const std::string& plugin_key,
bool skip_deps = false,
std::vector<std::string> capabilities_to_enable = std::vector<std::string>());
void enable_capability(const std::string& plugin_key, const std::string& capability_name, PluginCapabilityType type);
void disable_capability(const std::string& plugin_key, const std::string& capability_name, PluginCapabilityType type);
// Writes the .install_state.json sidecar for a currently-loaded plugin (enabled=true plus
// the current per-capability enabled flags). Source of truth for auto-load on next startup.
void write_loaded_plugin_install_state(const std::string& plugin_key);
bool inspect_local_plugin_package(const boost::filesystem::path& filepath,
PluginDescriptor& plugin_descriptor,
bool& existing_installation,
std::string& error) const;
bool install_plugin(const boost::filesystem::path& filepath, std::string& error);
bool install_plugin(const boost::filesystem::path& filepath,
PluginDescriptor& plugin_descriptor, std::string& error);
void clear_loaded_plugin_cloud_state(const std::string& plugin_key);
void update_loaded_plugin_key(const std::string& old_key, const std::string& new_key);
void set_cloud_user_id(const std::string& user_id) { m_cloud_user_id = user_id; }
void set_shutting_down() { m_shutting_down.store(true, std::memory_order_release); }
void unload_cloud_plugins();
void subscribe_on_load_callback(PluginLifecycleCompleteFn fn);
void subscribe_on_unload_callback(PluginLifecycleCompleteFn fn);
// Capability-level lifecycle callbacks, mirroring the package-level load/unload callbacks
// above but carrying the full capability identity. Fired for logical enable/disable and
// loaded-capability key migration; no Python module interaction.
using CapabilityLifecycleFn = std::function<void(const PluginCapabilityIdentifier&)>;
void subscribe_on_capability_load_callback(CapabilityLifecycleFn fn);
void subscribe_on_capability_unload_callback(CapabilityLifecycleFn fn);
private:
void load_plugin_impl(PluginCatalog& catalog,
const std::string& plugin_key,
bool skip_deps,
std::vector<std::string> capabilities_to_enable = std::vector<std::string>());
// Caller holds m_mutex. Removes only the exact typed identifiers owned by plugin.
std::vector<std::shared_ptr<LoadedPluginCapability>> extract_plugin_capabilities_locked(const LoadedPlugin& plugin);
void teardown_capabilities(std::vector<std::shared_ptr<LoadedPluginCapability>>& capabilities,
std::size_t lifecycle_count) const;
bool cancel_plugin_load_locked(const std::string& plugin_key);
bool is_plugin_load_cancelled_locked(const std::string& plugin_key) const;
void notify_plugin_load_state_changed(bool changed);
void run_on_load_callbacks(const std::string& plugin_key);
void run_on_unload_callbacks(const std::string& plugin_key);
void run_on_capability_load_callbacks(const PluginCapabilityIdentifier& id);
void run_on_capability_unload_callbacks(const PluginCapabilityIdentifier& id);
// Package store keyed by plugin_key. Capability wrappers live in the typed registry;
// packages retain their registration order through exact typed identifiers.
std::unordered_map<std::string /*plugin_key*/, LoadedPlugin> m_plugins;
using PluginCapabilityMap = std::unordered_map<PluginCapabilityIdentifier, std::shared_ptr<LoadedPluginCapability>>;
std::unordered_map<PluginCapabilityType, PluginCapabilityMap> m_plugin_capabilities;
PluginLoadInProgress m_plugin_load_in_progress;
PluginLoadErrors m_plugin_load_errors;
std::string m_cloud_user_id;
std::atomic<bool> m_shutting_down{false};
mutable std::mutex m_mutex;
mutable std::condition_variable m_plugin_load_cv;
std::unordered_map<CallbackType, std::vector<PluginLifecycleCompleteFn>> m_callbacks{};
std::unordered_map<CallbackType, std::vector<CapabilityLifecycleFn>> m_capability_callbacks{};
/*
callbacks:
on plugin load/unload
plugin discovery should always be blocking (with dialog)
all executions should be blocking (with dialog)
Currently, only script plugins should be cancellable.
*/
};
} // namespace Slic3r
+732
View File
@@ -0,0 +1,732 @@
#include "PluginManager.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include <pybind11/embed.h>
namespace py = pybind11;
#include "PythonPluginBridge.hpp"
#include "PythonPluginInterface.hpp"
#include "PluginFsUtils.hpp"
#include "PythonFileUtils.hpp"
#include <boost/log/trivial.hpp>
#include <boost/filesystem.hpp>
#include <mutex>
#include <algorithm>
#include <memory>
#include <chrono>
#include <slic3r/GUI/NotificationManager.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <vector>
#include <utility>
#include "PythonInterpreter.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "OrcaCloudServiceAgent.hpp"
namespace Slic3r {
namespace {
bool wait_for_plugin_catalog(const PluginCatalog& catalog, std::string& error)
{
error.clear();
if (!catalog.wait_for_discovery(std::chrono::milliseconds::max(), error)) {
if (error.empty())
error = "Plugin discovery is still running";
return false;
}
return true;
}
bool find_plugin_descriptor_by_key(const std::vector<PluginDescriptor>& catalog,
const std::vector<PluginDescriptor>& invalid_plugins,
const std::string& plugin_key,
PluginDescriptor& descriptor)
{
auto find_by_key = [&plugin_key](const PluginDescriptor& entry) { return entry.plugin_key == plugin_key; };
auto catalog_it = std::find_if(catalog.begin(), catalog.end(), find_by_key);
if (catalog_it != catalog.end()) {
descriptor = *catalog_it;
return true;
}
auto invalid_it = std::find_if(invalid_plugins.begin(), invalid_plugins.end(), find_by_key);
if (invalid_it != invalid_plugins.end()) {
descriptor = *invalid_it;
return true;
}
return false;
}
void remove_plugin_from_entries(std::vector<PluginDescriptor>& entries, const std::string& plugin_key)
{
entries.erase(std::remove_if(entries.begin(), entries.end(),
[&plugin_key](const PluginDescriptor& entry) { return entry.plugin_key == plugin_key; }),
entries.end());
}
void clear_plugin_cloud_state(std::vector<PluginDescriptor>& entries, const std::string& plugin_key)
{
for (auto& entry : entries) {
if (entry.plugin_key == plugin_key) {
entry.cloud.reset();
return;
}
}
}
} // namespace
LoadedPlugin::~LoadedPlugin()
{
if (!PythonInterpreter::instance().is_initialized()) {
module = nullptr;
return;
}
PythonGILState gil;
if (module != nullptr) {
Py_DECREF(module);
module = nullptr;
}
}
PluginManager& PluginManager::instance()
{
PythonInterpreter::instance();
static PluginManager inst;
return inst;
}
PluginManager::~PluginManager() { shutdown(); }
bool PluginManager::initialize()
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_initialized)
return true;
// Initialize the Python interpreter eagerly on the main thread.
// CPython must be initialized from the main thread; calling
// Py_InitializeFromConfig from a background thread (e.g. the
// load_plugin worker) causes heap corruption in CPython internals.
PythonInterpreter& interpreter = PythonInterpreter::instance();
if (!interpreter.is_initialized() && !interpreter.initialize()) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Failed to initialize Python interpreter: " << interpreter.last_error();
return false;
}
m_initialized = true;
// Persist auto-load / capability state to each plugin's .install_state.json sidecar.
// On load: write enabled=true plus current capability flags. On unload: flip enabled=false.
// The on-unload callback is skipped during shutdown (run_on_unload_callbacks is gated by
// !m_shutting_down), so app exit does not wipe the auto-load list.
m_loader.subscribe_on_load_callback([this](const std::string& key) {
m_loader.write_loaded_plugin_install_state(key);
});
m_loader.subscribe_on_unload_callback([this](const std::string& key) {
PluginDescriptor descriptor;
if (!m_catalog.try_get_plugin_descriptor(key, descriptor) || descriptor.plugin_root.empty())
return;
const boost::filesystem::path root(descriptor.plugin_root);
PluginInstallState st;
if (read_install_state(root, st)) {
st.enabled = false;
write_install_state(root, st);
}
});
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin manager initialized";
return true;
}
void PluginManager::shutdown()
{
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_initialized && !m_catalog.is_discovery_in_progress() && m_loader.is_idle_and_empty())
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": PluginManager shutdown enter";
// Signal the loader to reject new plugin loads before we drain.
m_loader.set_shutting_down();
std::string wait_error;
if (!m_catalog.wait_for_discovery(std::chrono::milliseconds::max(), wait_error) && !wait_error.empty())
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Plugin discovery did not finish cleanly during shutdown: " << wait_error;
// Wait for any in-progress plugin loads.
m_loader.wait_for_all_plugin_loads();
m_loader.unload_all_plugins();
PythonPluginBridge::instance().clear_pending_captures();
{
std::lock_guard<std::mutex> lock(m_mutex);
m_initialized = false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": PluginManager shutdown exit";
}
void PluginManager::discover_plugins(bool async, bool clear)
{
if (!initialize())
return;
if (clear)
m_catalog.clear_all_plugin_errors();
m_catalog.discover_plugins(async, clear);
}
void PluginManager::rescan_plugins()
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Rescanning plugins...";
std::string wait_error;
m_catalog.wait_for_discovery(std::chrono::milliseconds::max(), wait_error);
if (!initialize())
return;
m_catalog.clear_all_plugin_errors();
m_catalog.discover_plugins(false, true);
}
bool PluginManager::install_plugin(const boost::filesystem::path& filepath, std::string& error)
{
error.clear();
std::string wait_error;
if (!wait_for_plugin_catalog(m_catalog, wait_error)) {
error = wait_error;
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin installation failed while waiting for discovery: " << wait_error;
return false;
}
if (!m_loader.install_plugin(filepath, error))
return false;
return true;
}
bool PluginManager::install_plugin(const boost::filesystem::path& filepath, PluginDescriptor& plugin_descriptor, std::string& error)
{
error.clear();
std::string wait_error;
if (!wait_for_plugin_catalog(m_catalog, wait_error)) {
error = wait_error;
if (!plugin_descriptor.plugin_key.empty())
m_catalog.set_plugin_error(plugin_descriptor.plugin_key, error);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin installation failed while waiting for discovery: " << wait_error;
return false;
}
if (!m_loader.install_plugin(filepath, plugin_descriptor, error)) {
if (!plugin_descriptor.plugin_key.empty())
m_catalog.set_plugin_error(plugin_descriptor.plugin_key, error);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": " << error;
return false;
}
if (!plugin_descriptor.plugin_key.empty())
m_catalog.clear_plugin_error(plugin_descriptor.plugin_key);
return true;
}
bool PluginManager::set_plugin_error(const std::string& plugin_key, std::string error)
{
std::string wait_error;
if (!wait_for_plugin_catalog(m_catalog, wait_error))
return false;
PluginDescriptor descriptor;
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor))
return false;
descriptor.set_error(std::move(error));
return m_catalog.update_plugin_descriptor(plugin_key, descriptor);
}
bool PluginManager::clear_plugin_error(const std::string& plugin_key)
{
std::string wait_error;
if (!wait_for_plugin_catalog(m_catalog, wait_error))
return false;
PluginDescriptor descriptor;
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor))
return false;
if (!descriptor.is_metadata_valid())
return false;
descriptor.clear_error();
return m_catalog.update_plugin_descriptor(plugin_key, descriptor);
}
bool PluginManager::delete_plugin(const std::string& plugin_key, std::string& error)
{
if (!wait_for_plugin_catalog(m_catalog, error))
return false;
error.clear();
PluginDescriptor descriptor;
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
error = "Plugin not found: " + plugin_key;
return false;
}
if (!delete_installed_plugin_package(descriptor, error)) {
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Deleted plugin: " << plugin_key;
return true;
}
bool PluginManager::unsubscribe_cloud_plugin(const std::string& plugin_key, std::string& error)
{
if (!wait_for_plugin_catalog(m_catalog, error))
return false;
error.clear();
PluginDescriptor descriptor;
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
error = "Plugin not found: " + plugin_key;
return false;
}
if (!descriptor.is_cloud_plugin()) {
error = "Only cloud plugins can be unsubscribed.";
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
if (descriptor.cloud && descriptor.cloud->is_mine) {
error = "Cannot unsubscribe your own plugins. Use Delete from Cloud instead.";
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
if (!m_cloud_service.request_cloud_unsubscribe(descriptor, error)) {
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
return finalize_cloud_plugin_removal(descriptor, true, error);
}
bool PluginManager::delete_and_unsubscribe_cloud_plugin(const std::string& plugin_key, std::string& error)
{
if (!wait_for_plugin_catalog(m_catalog, error))
return false;
error.clear();
PluginDescriptor descriptor;
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
error = "Plugin not found: " + plugin_key;
return false;
}
if (!descriptor.is_cloud_plugin()) {
error = "Only cloud plugins can be deleted and unsubscribed.";
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
if (descriptor.cloud->is_mine) {
error = "Use Delete local and cloud for owned plugins.";
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
if (!m_cloud_service.request_cloud_unsubscribe(descriptor, error)) {
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
return finalize_cloud_plugin_removal(descriptor, false, error);
}
bool PluginManager::delete_mine_plugin_from_cloud(const std::string& plugin_key, std::string& error)
{
if (!wait_for_plugin_catalog(m_catalog, error))
return false;
error.clear();
PluginDescriptor descriptor;
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
error = "Plugin not found: " + plugin_key;
return false;
}
if (!descriptor.is_cloud_plugin()) {
error = "Only owned cloud plugins can be deleted from the cloud.";
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
if (!descriptor.cloud->is_mine) {
error = "Only your own plugins can be deleted from the cloud.";
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
if (!m_cloud_service.request_cloud_delete(descriptor, error)) {
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
return finalize_cloud_plugin_removal(descriptor, true, error);
}
bool PluginManager::delete_mine_local_and_cloud_plugin(const std::string& plugin_key, std::string& error)
{
if (!wait_for_plugin_catalog(m_catalog, error))
return false;
error.clear();
PluginDescriptor descriptor;
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
error = "Plugin not found: " + plugin_key;
return false;
}
if (!descriptor.is_cloud_plugin()) {
error = "Only owned cloud plugins can be deleted from local and cloud.";
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
if (!descriptor.cloud->is_mine) {
error = "Only your own plugins can be deleted from local and cloud.";
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
if (!m_cloud_service.request_cloud_delete(descriptor, error)) {
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
return finalize_cloud_plugin_removal(descriptor, false, error);
}
void PluginManager::fetch_plugins_from_cloud(std::vector<std::string>* out_not_found, std::vector<std::string>* out_unauthorized)
{
if (!m_cloud_service.can_fetch_cloud_plugins())
return;
std::vector<PluginDescriptor> cloud_list{};
std::vector<std::string> not_found{}, unauthorized{};
bool result = m_cloud_service.fetch_manifests_into_descriptors(cloud_list, not_found, unauthorized);
if (!result) {
if (GUI::wxGetApp().plater() != nullptr && GUI::wxGetApp().imgui()->display_initialized()) {
GUI::wxGetApp()
.plater()
->get_notification_manager()
->push_notification(GUI::NotificationType::CustomNotification,
GUI::NotificationManager::NotificationLevel::WarningNotificationLevel,
"Failed to fetch plugins from the cloud. See logs for details.");
}
}
m_catalog.update_cloud_catalog(cloud_list);
m_catalog.clear_cloud_plugin_unauthorized();
m_catalog.clear_cloud_plugin_not_found_errors();
// Mark cloud issues in the catalog so app UIs can reflect their shared state.
for (const auto& uuid : not_found)
m_catalog.mark_cloud_plugin_not_found(uuid);
for (const auto& uuid : unauthorized)
m_catalog.mark_cloud_plugin_unauthorized(uuid);
// Return the vectors to callers that need them (e.g. for notifications).
if (out_not_found)
*out_not_found = std::move(not_found);
if (out_unauthorized)
*out_unauthorized = std::move(unauthorized);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Cloud plugin fetch: " << cloud_list.size() << " plugins returned";
}
bool PluginManager::subscribe_and_install_cloud_plugin(const std::string& plugin_key, std::string& error)
{
error.clear();
const std::string plugin_uuid = is_uuid(plugin_key) ? plugin_key : std::string{};
if (plugin_uuid.empty()) {
error = "Cloud plugin key is missing UUID.";
return false;
}
if (!m_cloud_service.can_fetch_cloud_plugins()) {
error = "Sign in to OrcaCloud to install this plugin.";
return false;
}
PluginDescriptor descriptor;
bool found = m_catalog.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_cloud_plugin();
if (!found) {
// The plugin may already be subscribed or owned while the local catalog is stale.
fetch_plugins_from_cloud();
found = m_catalog.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_cloud_plugin();
}
if (!found) {
if (!m_cloud_service.request_cloud_subscribe(plugin_uuid, error))
return false;
fetch_plugins_from_cloud();
found = m_catalog.try_get_plugin_descriptor(plugin_key, descriptor) && descriptor.is_cloud_plugin();
if (!found) {
error = "Subscribed cloud plugin was not returned by OrcaCloud.";
return false;
}
}
if (descriptor.has_local_package())
return true;
return download_and_install_cloud_plugin(descriptor.plugin_key, descriptor.version, error);
}
bool PluginManager::download_and_install_cloud_plugin(const std::string& plugin_key, const std::string& version, std::string& error)
{
error.clear();
CloudPluginDownload download;
PluginDescriptor descriptor;
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
error = "Plugin not found: " + plugin_key;
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to find plugin key " << plugin_key;
return false;
}
m_catalog.clear_plugin_error(plugin_key);
const std::string requested_version = version.empty() ? descriptor.version : version;
if (!m_cloud_service.download_cloud_plugin(descriptor, requested_version, download, error)) {
if (error.empty())
error = "Failed to download cloud plugin.";
m_catalog.set_plugin_error(plugin_key, error);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": " << error;
return false;
}
// Ensure cloud plugins install to _subscribed/<user_id>/
if (auto agent = m_cloud_service.get_cloud_agent()) {
const std::string user_id = agent->get_user_id();
if (!user_id.empty())
m_loader.set_cloud_user_id(user_id);
}
// Record the version we just fetched from the cloud so install_plugin persists it to the
// install-state sidecar (the source of truth for the installed cloud version) instead of
// the local manifest/PEP723 header version.
descriptor.installed_version = requested_version;
if (!install_plugin(download.package_path, descriptor, error)) {
if (error.empty())
error = "Failed to install cloud plugin.";
m_catalog.set_plugin_error(plugin_key, error);
boost::system::error_code ec;
boost::filesystem::remove(download.package_path, ec);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": " << error;
return false;
}
PluginDescriptor updated_descriptor = descriptor;
if (!requested_version.empty())
updated_descriptor.version = requested_version;
// The version just downloaded and installed is now the locally installed version.
updated_descriptor.installed_version = requested_version.empty() ? descriptor.version : requested_version;
if (updated_descriptor.cloud.has_value()) {
updated_descriptor.cloud->installed = true;
updated_descriptor.cloud->update_available = false;
updated_descriptor.cloud->unauthorized = false;
}
updated_descriptor.clear_error();
updated_descriptor.set_unauthorized(false);
if (!m_catalog.update_plugin_descriptor(plugin_key, updated_descriptor)) {
error = "Plugin Manifest not found.";
m_catalog.set_plugin_error(plugin_key, error);
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Cloud plugin " << plugin_key
<< " downloaded successfully but failed to update plugin manifest. Manifest not found in catalog.";
return false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Cloud plugin " << plugin_key << " installed successfully";
return true;
}
bool PluginManager::finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error)
{
// Shared by all four cloud-removal entrypoints after the cloud-side request
// succeeds. Handles the common local follow-up of keeping a detached local
// copy, deleting local files, or dropping a cloud-only row from the catalog.
if (keep_local && plugin.has_local_package()) {
if (!keep_installed_plugin_as_local(plugin, error))
return false;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Removed cloud tracking, kept local copy: " << plugin.plugin_key;
return true;
}
if (plugin.has_local_package()) {
if (!delete_installed_plugin_package(plugin, error))
return false;
// Re-sync the cloud catalog so observers/UI see the updated cloud list
// after the local package has been removed.
fetch_plugins_from_cloud();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Deleted local package after cloud removal: " << plugin.plugin_key;
return true;
}
m_catalog.remove_plugin(plugin.plugin_key);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Removed cloud-only plugin from catalog: " << plugin.plugin_key;
return true;
}
bool PluginManager::delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error)
{
boost::filesystem::path resolved_root;
if (!resolve_allowed_plugin_root(plugin, m_catalog.get_plugin_directories(),
"Refusing to delete a plugin outside the known plugin directories.", resolved_root, error))
return false;
m_loader.unload_plugin(plugin.plugin_key, plugin.primary_capability_type());
if (!delete_plugin_root(resolved_root, plugin.plugin_key, error))
return false;
m_catalog.remove_plugin(plugin.plugin_key);
return true;
}
bool PluginManager::keep_installed_plugin_as_local(const PluginDescriptor& plugin_descriptor, std::string& error)
{
namespace fs = boost::filesystem;
boost::filesystem::path resolved_root;
if (!resolve_allowed_plugin_root(plugin_descriptor, m_catalog.get_plugin_directories(),
"Refusing to update a plugin outside the known plugin directories.", resolved_root, error))
return false;
const std::string old_key = plugin_descriptor.plugin_key;
// Generate new local key from the entry file stem (cloud → local conversion).
const std::string entry_stem = fs::path(plugin_descriptor.entry_path).stem().string();
const std::string new_key = make_local_plugin_key(
!entry_stem.empty() ? entry_stem : resolved_root.stem().string());
// Update metadata.
PluginDescriptor local_descriptor = plugin_descriptor;
local_descriptor.plugin_key = new_key;
local_descriptor.cloud = std::nullopt;
local_descriptor.clear_error();
if (!write_install_state(resolved_root, local_descriptor)) {
error = "Failed to update plugin install state: " + (resolved_root / INSTALL_STATE_FILE).string();
return false;
}
// Update catalog entry key in-memory.
{
std::lock_guard<std::mutex> lock(m_mutex);
auto& catalog_entries = const_cast<std::vector<PluginDescriptor>&>(m_catalog.get_plugin_catalog());
for (auto& entry : catalog_entries) {
if (entry.plugin_key == old_key) {
entry.plugin_key = new_key;
entry.cloud.reset();
entry.clear_error();
break;
}
}
}
// Update loaded plugin manifest key if currently loaded.
m_loader.update_loaded_plugin_key(old_key, new_key);
// Auto-load state for the new local key is carried by the .install_state.json sidecar
// written above with the new local descriptor.
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Transitioned plugin from " << old_key << " to " << new_key;
return true;
}
bool PluginManager::update_cloud_plugin(const std::string& plugin_key, std::string& error, std::string version)
{
error.clear();
// delete local plugin file and download newer version
PluginDescriptor descriptor;
if (!m_catalog.try_get_plugin_descriptor(plugin_key, descriptor)) {
error = "Plugin not found: " + plugin_key;
return false;
}
if (!descriptor.is_cloud_plugin()) {
error = "Only cloud plugins can be updated.";
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
// Empty version should represent that we are trying to update the current plugin.
// So if the version is empty but there isn't an update available, the intention is unclear.
if (version.empty()) {
if (descriptor.cloud->update_available)
version = descriptor.latest_available_version();
else {
error = "Trying to update plugin with no available update. Version is empty.";
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
}
m_catalog.clear_plugin_error(plugin_key);
if (descriptor.has_local_package()) {
boost::filesystem::path resolved_root;
if (!resolve_allowed_plugin_root(descriptor, m_catalog.get_plugin_directories(),
"Refusing to delete a plugin outside the known plugin directories.", resolved_root, error)) {
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
m_loader.unload_plugin(plugin_key, descriptor.primary_capability_type());
if (!delete_plugin_root(resolved_root, plugin_key, error)) {
m_catalog.set_plugin_error(plugin_key, error);
return false;
}
}
if (!download_and_install_cloud_plugin(plugin_key, version, error)) {
m_catalog.set_plugin_error(plugin_key, error);
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": " << error;
return false;
}
m_catalog.clear_plugin_error(plugin_key);
return true;
}
} // namespace Slic3r
+163
View File
@@ -0,0 +1,163 @@
#ifndef slic3r_PluginManager_hpp_
#define slic3r_PluginManager_hpp_
#include <boost/filesystem/path.hpp>
#include <algorithm>
#include <chrono>
#include <functional>
#include <libslic3r/Config.hpp>
#include <memory>
#include <mutex>
#include <optional>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <string>
#include <unordered_map>
#include <vector>
#include "CloudPluginService.hpp"
#include "PluginCatalog.hpp"
#include "PluginLoader.hpp"
#include "PluginDescriptor.hpp"
namespace Slic3r {
class OrcaCloudServiceAgent;
class PluginManager
{
public:
static PluginManager& instance();
~PluginManager();
// Initialize plugin system (no longer initializes Python — that happens lazily on first load_plugin)
bool initialize();
// Stop discovery and unload Python plugin objects before Python finalizes.
void shutdown();
// Discover and scan plugins from standard directories (manifest-only, no Python loading).
// Runs on a worker thread when async=true.
void discover_plugins(bool async = false, bool clear = false);
// fetches plugins from the cloud
void fetch_plugins_from_cloud(std::vector<std::string>* out_not_found = nullptr,
std::vector<std::string>* out_unauthorized = nullptr);
// Download and install a cloud plugin from its download_url.
// Returns an error string on failure (empty URL, network down, etc.).
// On success returns an empty string.
bool download_and_install_cloud_plugin(const std::string& plugin_key, const std::string& version, std::string& error);
bool subscribe_and_install_cloud_plugin(const std::string& plugin_key, std::string& error);
// Manually trigger manifest-only rescan of plugins. Blocks until discovery is complete.
void rescan_plugins();
PluginCatalog& get_catalog() { return m_catalog; }
const PluginCatalog& get_catalog() const { return m_catalog; }
PluginLoader& get_loader() { return m_loader; }
const PluginLoader& get_loader() const { return m_loader; }
void set_cloud_agent(std::shared_ptr<OrcaCloudServiceAgent> agent) { m_cloud_service.set_cloud_agent(std::move(agent)); }
bool install_plugin(const boost::filesystem::path& filepath, std::string& error);
bool install_plugin(const boost::filesystem::path& filepath, PluginDescriptor& plugin_descriptor, std::string& error);
bool set_plugin_error(const std::string& plugin_key, std::string error);
bool clear_plugin_error(const std::string& plugin_key);
// If the version is empty, take the latest version.
bool update_cloud_plugin(const std::string& plugin_key, std::string& error, std::string version = "");
bool delete_plugin(const std::string& plugin_key, std::string& error);
bool unsubscribe_cloud_plugin(const std::string& plugin_key, std::string& error);
bool delete_and_unsubscribe_cloud_plugin(const std::string& plugin_key, std::string& error);
bool delete_mine_plugin_from_cloud(const std::string& plugin_key, std::string& error);
bool delete_mine_local_and_cloud_plugin(const std::string& plugin_key, std::string& error);
private:
PluginManager() = default;
PluginManager(const PluginManager&) = delete;
PluginManager& operator=(const PluginManager&) = delete;
bool finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error);
bool delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error);
bool keep_installed_plugin_as_local(const PluginDescriptor& plugin_descriptor, std::string& error);
bool m_initialized = false;
CloudPluginService m_cloud_service;
PluginCatalog m_catalog;
PluginLoader m_loader;
mutable std::mutex m_mutex;
std::unordered_map<PluginCapabilityType, std::vector<std::function<void(const std::vector<PluginDescriptor>&)>>> m_loaded_plugin_changed_callbacks;
};
template <typename T>
void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
const ConfigOptionStrings* plugins,
PluginCapabilityType type,
std::function<void(std::shared_ptr<T>, const PluginCapabilityRef&)> execute)
{
PluginManager& plugin_mgr = PluginManager::instance();
const bool has_any = std::any_of(capabilities.values.begin(), capabilities.values.end(),
[](const std::string& s) { return !s.empty(); });
if (has_any && !plugin_mgr.get_loader().wait_for_all_plugin_loads(std::chrono::seconds(10))) {
BOOST_LOG_TRIVIAL(warning) << "Post-process: timed out waiting for plugin loads; unresolved capabilities will be skipped";
}
for (const std::string& capability : capabilities.values) {
if (capability.empty())
continue;
std::shared_ptr<LoadedPluginCapability> cap;
std::string cap_name, plugin_key;
std::optional<PluginCapabilityRef> ref;
if (plugins != nullptr) {
for (const std::string& plugin_ref : plugins->values) {
auto parsed = Slic3r::parse_capability_ref(plugin_ref);
if (parsed && parsed->capability_name == capability) {
ref = std::move(parsed);
break;
}
}
}
if (!ref) {
BOOST_LOG_TRIVIAL(warning) << "Post-processing: no plugin reference found for capability '" << capability << "'; skipping";
continue;
}
cap_name = ref->capability_name;
plugin_key = ref->uuid.empty() ? ref->name : ref->uuid;
cap = plugin_mgr.get_loader().get_plugin_capability_by_name(plugin_key, type, cap_name);
if (!cap) {
BOOST_LOG_TRIVIAL(warning) << "Post-processing: no loaded capability '" << cap_name
<< "' for plugin '" << plugin_key << "'; skipping";
continue;
}
if (!cap->enabled) {
BOOST_LOG_TRIVIAL(warning) << "Post-processing: capability '" << cap_name
<< "' for plugin '" << plugin_key << "' is disabled; skipping";
continue;
}
auto plugin_capability = std::dynamic_pointer_cast<T>(cap->instance);
if (!plugin_capability) {
BOOST_LOG_TRIVIAL(warning) << "Post-processing: capability '" << cap_name
<< "' (plugin_key=" << cap->plugin_key
<< ") is not a " << plugin_capability_type_to_string(type) << "; skipping";
continue;
}
execute(plugin_capability, ref.value());
}
}
} // namespace Slic3r
#endif /* slic3r_PluginManager_hpp_ */
+401
View File
@@ -0,0 +1,401 @@
#include "PluginResolver.hpp"
#include "PluginManager.hpp"
#include "../Utils/Http.hpp"
#include "../Utils/OrcaCloudServiceAgent.hpp"
#include "../GUI/GUI.hpp"
#include "../GUI/GUI_App.hpp"
#include "../GUI/I18N.hpp"
#include "../GUI/Plater.hpp"
#include "../GUI/NotificationManager.hpp"
#include <boost/log/trivial.hpp>
#include <libslic3r/Config.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <vector>
#include <wx/utils.h>
#include <algorithm>
#include <chrono>
#include <map>
#include <mutex>
#include <thread>
#include <unordered_map>
namespace Slic3r {
namespace {
// Return the name of the tracked option in `preset` whose value references `ref`'s capability, or
// an empty string when no active option uses it. The result doubles as the "Jump to" target and as
// the signal that the plugin is still required: a missing plugin with no referencing option is
// considered resolved and dropped from the missing set.
std::string find_option_for_capability(Preset::Type type, const Preset& preset, const PluginCapabilityRef& ref)
{
if (type != Preset::TYPE_PRINT && type != Preset::TYPE_PRINTER && type != Preset::TYPE_FILAMENT)
return {};
// Plugin-bearing options opt in via ConfigOptionDef::support_plugin, so scan the preset's
// definition rather than maintaining a hardcoded per-type field list. A typed preset's config
// only contains keys for its own type, so this naturally stays scoped to `type`.
const ConfigDef* def = preset.config.def();
if (def == nullptr)
return {};
const auto matches_ref = [&ref](const std::string& value) {
return value == ref.capability_name;
};
for (const std::string& field : preset.config.keys()) {
const ConfigOptionDef* opt_def = def->get(field);
if (opt_def == nullptr || !opt_def->support_plugin)
continue;
const ConfigOption* option = preset.config.option(field);
if (option == nullptr)
continue;
if (const auto* string_option = dynamic_cast<const ConfigOptionString*>(option)) {
if (string_option->value == ref.capability_name)
return field;
continue;
}
if (const auto* vector_option = dynamic_cast<const ConfigOptionVectorBase*>(option)) {
const std::vector<std::string> values = vector_option->vserialize();
if (std::any_of(values.begin(), values.end(), matches_ref))
return field;
}
}
// printer_agent stores AgentInfo::id, so a missing plugin cannot be reverse-mapped through
// the runtime registry. If no regular printer plugin field matched, assume printer_agent.
if (type == Preset::Type::TYPE_PRINTER && preset.config.has("printer_agent"))
return "printer_agent";
return {};
}
} // namespace
// One missing-plugin set per tracked preset type, keyed by the full "name;uuid;capability" ref.
// Only TYPE_PRINT (process), TYPE_PRINTER (machine) and TYPE_FILAMENT are tracked.
static std::map<Preset::Type, std::unordered_map<std::string, MissingPlugin>> s_missing;
static std::mutex s_missing_mutex;
// Installed-but-inactive capabilities (not loaded, or loaded-but-disabled); resolvable locally.
static std::map<Preset::Type, std::unordered_map<std::string, MissingPlugin>> s_inactive;
// Installed+loaded but capability absent; not resolvable by activation. Both share s_missing_mutex.
static std::map<Preset::Type, std::unordered_map<std::string, MissingPlugin>> s_broken;
static bool is_tracked_type(Preset::Type type)
{
return type == Preset::TYPE_PRINT || type == Preset::TYPE_PRINTER || type == Preset::TYPE_FILAMENT;
}
static std::string resolve_cloud_base_url()
{
std::string cloud_base_url = "https://cloud.orcaslicer.com";
if (auto agent = GUI::wxGetApp().getAgent()) {
if (auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent())) {
if (!orca_agent->get_cloud_base_url().empty())
cloud_base_url = orca_agent->get_cloud_base_url();
}
}
return cloud_base_url;
}
std::string create_full_ref(const PluginCapabilityRef& ref) { return ref.name + ';' + ref.uuid + ';' + ref.capability_name; }
std::string resolve_recovery_url(const PluginCapabilityRef& ref)
{
return resolve_cloud_base_url() + "/app/plugins/plugin-hub?search=" + Http::url_encode(ref.name);
}
// Reports whether the loaded plugin currently exposes the referenced capability (in any enabled
// state) and whether that capability is enabled. Returns {false, false} when the plugin is not
// loaded or does not provide the capability.
static std::pair<bool, bool> loaded_capability_state(const std::string& plugin_key, const PluginCapabilityRef& ref)
{
bool present = false, enabled = false;
PluginLoader& loader = PluginManager::instance().get_loader();
for (const auto& capability : loader.get_loaded_plugin_capabilities(plugin_key))
if (capability && capability->name == ref.capability_name) {
present = true;
if (capability->enabled)
enabled = true;
}
return {present, enabled};
}
void refresh_missing_plugins(const PresetBundle& preset_bundle)
{
const auto manifest_of = [](const Preset& preset) { return dynamic_cast<const ConfigOptionStrings*>(preset.config.option("plugins")); };
const Preset& print_preset = preset_bundle.prints.get_edited_preset();
refresh_missing_plugins(Preset::TYPE_PRINT, manifest_of(print_preset), &print_preset);
const Preset& printer_preset = preset_bundle.printers.get_edited_preset();
refresh_missing_plugins(Preset::TYPE_PRINTER, manifest_of(printer_preset), &printer_preset);
// Filament plugins (if any) are the union over all selected filament presets.
ConfigOptionStrings filament_manifest;
for (const std::string& filament_name : preset_bundle.filament_presets) {
const Preset* filament = preset_bundle.filaments.find_preset(filament_name);
if (!filament)
continue;
if (const auto* opt = dynamic_cast<const ConfigOptionStrings*>(filament->config.option("plugins")))
filament_manifest.values.insert(filament_manifest.values.end(), opt->values.begin(), opt->values.end());
}
refresh_missing_plugins(Preset::TYPE_FILAMENT, &filament_manifest);
}
void refresh_missing_plugins(Preset::Type type, const ConfigOptionStrings* manifest, const Preset* preset)
{
if (!is_tracked_type(type))
return;
std::lock_guard<std::mutex> lock(s_missing_mutex);
auto& missing_set = s_missing[type];
auto& inactive_set = s_inactive[type];
auto& broken_set = s_broken[type];
missing_set.clear();
inactive_set.clear();
broken_set.clear();
if (manifest == nullptr)
return;
PluginLoader& loader = PluginManager::instance().get_loader();
const PluginCatalog& catalog = PluginManager::instance().get_catalog();
for (const std::string& entry : manifest->values) {
const auto ref = parse_capability_ref(entry);
if (!ref)
continue;
// Cloud plugins resolve by UUID, local plugins by plugin_key (the first field).
const std::string key = ref->uuid.empty() ? ref->name : ref->uuid;
if (key.empty())
continue;
PluginDescriptor descriptor;
const bool in_catalog = catalog.try_get_plugin_descriptor(key, descriptor);
const bool installed = in_catalog && descriptor.has_local_package();
const bool loaded = installed && loader.is_plugin_loaded(descriptor.plugin_key);
const auto cap_state = installed ? loaded_capability_state(descriptor.plugin_key, *ref)
: std::pair<bool, bool>{false, false};
const bool cap_present = cap_state.first;
const bool cap_enabled = cap_state.second;
if (cap_enabled)
continue; // active and enabled — nothing to resolve
std::string opt = preset != nullptr ? find_option_for_capability(type, *preset, *ref) : std::string();
if (opt.empty())
continue;
if (!installed) {
// Not on disk — needs download/install (existing behavior).
std::string recovery_url = ref->uuid.empty() ? resolve_recovery_url(*ref) : std::string();
missing_set.emplace(entry, MissingPlugin{*ref, std::move(recovery_url), std::move(opt), type, PluginCapabilityType::Unknown});
} else if (!loaded || cap_present) {
// Installed but not loaded yet (optimistic), or loaded but capability disabled — activatable.
inactive_set.emplace(entry, MissingPlugin{*ref, std::string(), std::move(opt), type, PluginCapabilityType::Unknown});
} else {
// Loaded but the capability is absent — activation cannot fix it; offer a browse/update link.
broken_set.emplace(entry, MissingPlugin{*ref, resolve_recovery_url(*ref), std::move(opt), type, PluginCapabilityType::Unknown});
}
}
}
std::vector<MissingPlugin> get_missing_cloud_plugins()
{
std::vector<MissingPlugin> out;
std::lock_guard<std::mutex> lock(s_missing_mutex);
for (const auto& [type, set] : s_missing)
for (const auto& [ref_str, missing] : set)
if (!missing.ref.uuid.empty())
out.push_back(missing);
return out;
}
std::vector<MissingPlugin> get_missing_local_plugins()
{
std::vector<MissingPlugin> out;
std::lock_guard<std::mutex> lock(s_missing_mutex);
for (const auto& [type, set] : s_missing)
for (const auto& [ref_str, missing] : set)
if (missing.ref.uuid.empty())
out.push_back(missing);
return out;
}
bool has_missing_plugins()
{
std::lock_guard<std::mutex> lock(s_missing_mutex);
for (const auto& [type, set] : s_missing)
if (!set.empty())
return true;
return false;
}
std::vector<MissingPlugin> get_inactive_plugins()
{
std::vector<MissingPlugin> out;
std::lock_guard<std::mutex> lock(s_missing_mutex);
for (const auto& [type, set] : s_inactive)
for (const auto& [ref_str, plugin] : set)
out.push_back(plugin);
return out;
}
bool has_inactive_plugins()
{
std::lock_guard<std::mutex> lock(s_missing_mutex);
for (const auto& [type, set] : s_inactive)
if (!set.empty())
return true;
return false;
}
std::vector<MissingPlugin> get_broken_plugins()
{
std::vector<MissingPlugin> out;
std::lock_guard<std::mutex> lock(s_missing_mutex);
for (const auto& [type, set] : s_broken)
for (const auto& [ref_str, plugin] : set)
out.push_back(plugin);
return out;
}
bool has_broken_plugins()
{
std::lock_guard<std::mutex> lock(s_missing_mutex);
for (const auto& [type, set] : s_broken)
if (!set.empty())
return true;
return false;
}
static void report_install_failure(const std::string& message)
{
GUI::wxGetApp().CallAfter([message]() {
if (GUI::Plater* plater = GUI::wxGetApp().plater())
plater->get_notification_manager()->push_notification(GUI::NotificationType::OrcaCloudAPIError,
GUI::NotificationManager::NotificationLevel::ErrorNotificationLevel,
_u8L("Plugin installation failed") + ": " + message);
});
}
void resolve_missing_plugins(const std::vector<std::string>& refs, PluginInstallProgress progress)
{
// Collect the unique cloud UUIDs to install; local refs are handled via the browser flow.
std::vector<std::string> uuids;
for (const std::string& r : refs) {
const auto ref = parse_capability_ref(r);
if (ref && !ref->uuid.empty() && std::find(uuids.begin(), uuids.end(), ref->uuid) == uuids.end())
uuids.push_back(ref->uuid);
}
if (uuids.empty()) {
if (progress.on_finished)
progress.on_finished();
return;
}
// subscribe_and_install_cloud_plugin blocks on network + load, so run off the UI thread. On
// success the capability-load callback re-validates the plate and clears the notification.
std::thread worker([uuids, progress = std::move(progress)]() {
PluginManager& mgr = PluginManager::instance();
const std::size_t total = uuids.size();
for (std::size_t i = 0; i < total; ++i) {
if (progress.is_cancelled && progress.is_cancelled())
break;
const std::string& uuid = uuids[i];
// Use a friendly name for the progress message when the catalog already knows it.
std::string display_name = uuid;
PluginDescriptor known;
if (mgr.get_catalog().try_get_plugin_descriptor(uuid, known) && !known.name.empty())
display_name = known.name;
if (progress.on_plugin_begin)
progress.on_plugin_begin(display_name, i, total);
std::string error;
if (!mgr.subscribe_and_install_cloud_plugin(uuid, error)) {
report_install_failure(error.empty() ? uuid : (uuid + ": " + error));
continue;
}
PluginDescriptor descriptor;
if (!mgr.get_catalog().try_get_plugin_descriptor(uuid, descriptor)) {
report_install_failure(uuid + ": installed plugin was not found in the catalog.");
continue;
}
PluginLoader& loader = mgr.get_loader();
loader.load_plugin(mgr.get_catalog(), descriptor.plugin_key, false);
if (!loader.wait_for_plugin_load(descriptor.plugin_key, std::chrono::minutes(5), error) ||
!loader.is_plugin_loaded(descriptor.plugin_key)) {
report_install_failure(descriptor.name + ": " + (error.empty() ? "plugin failed to load." : error));
}
}
if (progress.on_finished)
progress.on_finished();
});
worker.detach();
}
void resolve_inactive_plugins(const std::vector<std::string>& refs)
{
PluginManager& mgr = PluginManager::instance();
PluginCatalog& catalog = mgr.get_catalog();
// Group the requested capabilities by owning plugin so each plugin is loaded once with the full
// set to enable.
std::map<std::string, std::vector<std::string>> by_plugin;
for (const std::string& r : refs) {
const auto ref = parse_capability_ref(r);
if (!ref)
continue;
const std::string key = ref->uuid.empty() ? ref->name : ref->uuid;
PluginDescriptor descriptor;
if (!catalog.try_get_plugin_descriptor(key, descriptor) || !descriptor.has_local_package())
continue;
by_plugin[descriptor.plugin_key].push_back(ref->capability_name);
}
if (by_plugin.empty())
return;
// load_plugin loads+enables a not-loaded plugin (async) and enables the listed capabilities on an
// already-loaded one. The fresh-load path does NOT fire the capability-load callback the GUI uses
// to clear the notification, so wait for each load off the UI thread and then re-validate once —
// mirroring the cloud-install flow. This clears the inactive notification, or flips it to broken
// if the loaded plugin turns out not to provide the capability.
std::vector<std::pair<std::string, std::vector<std::string>>> work(by_plugin.begin(), by_plugin.end());
std::thread([work = std::move(work)]() {
PluginManager& mgr = PluginManager::instance();
PluginLoader& loader = mgr.get_loader();
PluginCatalog& catalog = mgr.get_catalog();
for (auto& [plugin_key, capabilities] : work) {
loader.load_plugin(catalog, plugin_key, /*skip_deps=*/false, capabilities);
std::string error;
loader.wait_for_plugin_load(plugin_key, std::chrono::minutes(5), error);
}
GUI::wxGetApp().CallAfter([]() {
if (GUI::Plater* plater = GUI::wxGetApp().plater())
plater->revalidate_current_plate_if_plugins_missing();
});
}).detach();
}
void open_missing_plugins_on_cloud(const std::vector<std::string>& local_refs)
{
// One missing plugin: deep-link a search for it. Multiple: just open the plugin hub.
if (local_refs.size() == 1) {
if (const auto ref = parse_capability_ref(local_refs.front())) {
wxLaunchDefaultBrowser(GUI::from_u8(resolve_recovery_url(*ref)), wxBROWSER_NEW_WINDOW);
return;
}
}
wxLaunchDefaultBrowser(GUI::from_u8(resolve_cloud_base_url() + "/app/plugins/plugin-hub"), wxBROWSER_NEW_WINDOW);
}
bool check_capability_in_use(const std::string &capability_refs) {
return false;
}
} // namespace Slic3r
+88
View File
@@ -0,0 +1,88 @@
#ifndef slic3r_PluginResolver_hpp_
#define slic3r_PluginResolver_hpp_
#include <libslic3r/Config.hpp> // PluginCapabilityRef, parse_capability_ref
#include <libslic3r/Preset.hpp> // Preset::Type
#include <libslic3r/PresetBundle.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp> // PluginCapabilityType
#include <cstddef>
#include <functional>
#include <string>
#include <vector>
namespace Slic3r {
// A plugin capability referenced by an active preset but not currently available (installed and
// loadable) on this machine.
struct MissingPlugin
{
PluginCapabilityRef ref; // parsed "name;uuid;capability"
std::string recovery_url; // OrcaCloud search URL (local plugins only)
std::string opt;
Preset::Type opt_type;
PluginCapabilityType type{PluginCapabilityType::Unknown};
};
// Rebuild the missing-plugin set owned by a single preset type from that preset's "plugins"
// manifest, comparing each ref against the live plugin catalog and loaded/enabled capabilities. A
// null/empty manifest clears the set for that type. Only TYPE_PRINT (process), TYPE_PRINTER
// (machine) and TYPE_FILAMENT are tracked; other types are ignored.
void refresh_missing_plugins(Preset::Type type, const ConfigOptionStrings* manifest, const Preset* preset = nullptr);
void refresh_missing_plugins(const PresetBundle& preset_bundle);
// Aggregate queries across all tracked preset types. Each entry is a full "name;uuid;capability"
// reference. Cloud refs carry a non-empty UUID; local refs do not.
std::vector<MissingPlugin> get_missing_cloud_plugins();
std::vector<MissingPlugin> get_missing_local_plugins();
bool has_missing_plugins();
// Installed-but-inactive capabilities: the plugin has a local package but the referenced capability
// is not active because the plugin is not loaded, or it is loaded but the capability is disabled.
// Resolved locally by loading the plugin and/or enabling the capability — no download.
std::vector<MissingPlugin> get_inactive_plugins();
bool has_inactive_plugins();
// Broken references: the plugin is installed AND loaded but does not provide the referenced
// capability at all (renamed/removed/outdated plugin). Activation cannot fix these; surfaced as an
// informational notification pointing the user at OrcaCloud to update the plugin.
std::vector<MissingPlugin> get_broken_plugins();
bool has_broken_plugins();
// Resolution actions invoked from the missing-plugin notifications:
// - cloud refs are subscribed/installed and loaded on a detached worker thread; failures are
// reported through a non-blocking notification. Non-cloud refs are ignored.
// Optional progress hook for the cloud install worker. All three callbacks fire on the worker
// thread; implementations must only touch thread-safe state or marshal to the UI thread.
struct PluginInstallProgress
{
// Fired before each plugin's install begins. `index` is 0-based; `total` is the plugin count.
std::function<void(const std::string& name, std::size_t index, std::size_t total)> on_plugin_begin;
// Polled between plugins; returning true stops the loop before the next plugin starts.
std::function<bool()> is_cancelled;
// Fired exactly once when the loop ends (all installed, failed, or cancelled).
std::function<void()> on_finished;
};
// Cloud refs only; local refs are handled via the browser flow. `progress` is optional — a
// default-constructed value preserves the previous silent behavior.
void resolve_missing_plugins(const std::vector<std::string>& refs,
PluginInstallProgress progress = {});
// Activate inactive plugins: load each referenced plugin (passing the capabilities to enable) and/or
// enable already-loaded-but-disabled capabilities. Local only — no network. The loads run on a
// background worker that waits for them and then re-validates the plate, clearing the notification
// (or reclassifying the ref as broken if the loaded plugin turns out not to provide the capability).
void resolve_inactive_plugins(const std::vector<std::string>& refs);
// - local refs are opened on the OrcaCloud plugin hub (search when exactly one ref, hub otherwise).
void open_missing_plugins_on_cloud(const std::vector<std::string>& local_refs);
std::string create_full_ref(const PluginCapabilityRef& ref);
std::string resolve_recovery_url(const PluginCapabilityRef& ref);
bool check_capability_in_use(const std::string& capability_refs);
} // namespace Slic3r
#endif
+27
View File
@@ -0,0 +1,27 @@
#ifndef slic3r_PyPluginPackage_hpp_
#define slic3r_PyPluginPackage_hpp_
#include <pybind11/pybind11.h>
namespace Slic3r {
// Package base class: a plugin file subclasses orca.base and overrides
// register_capabilities() to call orca.register_capability() for each capability.
class PyPluginPackage
{
public:
virtual ~PyPluginPackage() = default;
virtual void register_capabilities() {}
};
class PyPluginPackageTrampoline : public PyPluginPackage
{
public:
using PyPluginPackage::PyPluginPackage;
void register_capabilities() override { PYBIND11_OVERRIDE(void, PyPluginPackage, register_capabilities); }
};
} // namespace Slic3r
#endif
+110
View File
@@ -0,0 +1,110 @@
#ifndef slic3r_PyPluginTrampoline_hpp_
#define slic3r_PyPluginTrampoline_hpp_
#include <pybind11/embed.h>
#include <optional>
#include "PythonPluginInterface.hpp"
#include "PythonInterpreter.hpp"
#include "PluginAuditManager.hpp"
// Trampoline variants of pybind11's override macros. Every C++->Python plugin call
// crosses through a trampoline method, so this single boundary is where we (1) log the
// full Python traceback (to sys.stderr -> session log) and rethrow the exception intact,
// and (2) open the plugin's filesystem audit scope for the duration of the call.
// We catch ONLY error_already_set (a Python-side raise); other pybind11_fail/runtime_error
// like a pure-virtual-missing failure must keep their own path and are deliberately not
// caught here.
// Logs (and rethrows) a Python exception from a pybind11 override call, preserving the
// traceback. Internal helper shared by the public macros below and by trampolines that
// manage their own audit scope (e.g. the G-code plugin).
#define ORCA_PY_LOGGED_OVERRIDE_BODY(override_call) \
try { \
override_call; \
} catch (pybind11::error_already_set & err) { \
::Slic3r::log_python_exception_keep(err); \
throw; \
}
// Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call
// when this trampoline instance carries a non-empty audit plugin key. Declares a local
// `_orca_audit_scope`.
#define ORCA_PY_AUDIT_SCOPE(mode) \
std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \
if (const std::string& _orca_audit_key = this->audit_plugin_key(); \
!_orca_audit_key.empty()) \
_orca_audit_scope.emplace(_orca_audit_key, mode)
#define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \
do { \
ORCA_PY_AUDIT_SCOPE(mode); \
if (_orca_audit_scope) \
audit_setup(); \
ORCA_PY_LOGGED_OVERRIDE_BODY(override_macro(ret, base, name, ##__VA_ARGS__)); \
} while (0)
namespace Slic3r {
template<class Base> class PyPluginCommonTrampoline : public Base
{
public:
using Base::Base;
// get_name is required on all capabilities — Python subclass must implement it.
std::string get_name() const override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE_PURE,
std::string,
Base,
get_name);
}
// All plugins may define their own on_load/unload functions.
void on_load() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE,
void,
Base,
on_load);
}
void on_unload() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE,
void,
Base,
on_unload);
}
};
class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline<PluginCapabilityInterface>
{
public:
using PyPluginCommonTrampoline<PluginCapabilityInterface>::PyPluginCommonTrampoline;
// get_name is implemented in PyPluginCommonTrampoline (PYBIND11_OVERRIDE_PURE).
PluginCapabilityType get_type() const override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE,
PluginCapabilityType,
PluginCapabilityInterface,
get_type);
}
};
} // namespace Slic3r
#endif
+955
View File
@@ -0,0 +1,955 @@
#include "PythonFileUtils.hpp"
#include "PluginFsUtils.hpp"
#include "PythonInterpreter.hpp"
#include "libslic3r/Utils.hpp"
#include "PluginDescriptor.hpp"
#include "libslic3r/miniz_extension.hpp"
#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <sstream>
#include <unordered_set>
#include <vector>
#ifdef WIN32
#include <boost/locale/encoding_utf.hpp>
#endif
namespace Slic3r {
namespace {
bool is_safe_archive_entry_path(const boost::filesystem::path& path)
{
if (!is_safe_relative_path(path))
return false;
for (const auto& part : path) {
const std::string token = part.string();
if (token.empty() || token == "." || token == "..")
return false;
}
return true;
}
std::string decode_zip_entry_extra_path(const std::string& extra, const std::string& fallback)
{
const char* p = extra.data();
const char* e = p + extra.length();
while (p + 4 <= e) {
const auto len = static_cast<std::uint16_t>(static_cast<unsigned char>(p[2])) |
static_cast<std::uint16_t>(static_cast<unsigned char>(p[3]) << 8);
if (p[0] == '\x75' && p[1] == '\x70' && len >= 5 && p + 4 + len <= e && p[4] == '\x01')
return std::string(p + 9, p + 4 + len);
p += 4 + len;
}
return decode_path(fallback.c_str());
}
std::string zip_entry_name(mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)
{
if (stat.m_is_utf8)
return stat.m_filename;
std::string extra(1024, 0);
const size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size());
return decode_zip_entry_extra_path(extra.substr(0, n), stat.m_filename);
}
std::string normalize_zip_entry_name(std::string entry_name)
{
std::replace(entry_name.begin(), entry_name.end(), '\\', '/');
while (!entry_name.empty() && entry_name.back() == '/')
entry_name.pop_back();
return entry_name;
}
struct ZipReaderGuard
{
mz_zip_archive archive;
bool opened = false;
ZipReaderGuard() { mz_zip_zero_struct(&archive); }
~ZipReaderGuard()
{
if (opened)
close_zip_reader(&archive);
}
};
} // namespace
bool is_valid_plugin_id(const std::string& id)
{
if (id.empty())
return false;
if (id == "." || id == ".." || id[0] == '.' || id.rfind("__", 0) == 0)
return false;
for (unsigned char ch : id) {
if (std::isalnum(ch) || ch == '_' || ch == '-' || ch == '.')
continue;
return false;
}
return true;
}
namespace {
// RAII helper to free heap-allocated memory from miniz.
struct MzHeapFree {
void* ptr = nullptr;
~MzHeapFree() { if (ptr) std::free(ptr); }
};
// Read a text file from within a zip archive into a string.
// Returns true on success, false if the file is not found or cannot be read.
bool read_zip_text_file(mz_zip_archive& archive, const char* filename, std::string& out, std::string& error)
{
size_t size = 0;
void* data = mz_zip_reader_extract_file_to_heap(&archive, filename, &size, 0);
if (!data) {
error = std::string("Wheel does not contain ") + filename;
return false;
}
MzHeapFree guard{data};
out.assign(static_cast<const char*>(data), size);
return true;
}
// TOML section parsing states.
enum class TomlSection { Root, OrcaPlugin, InDepsArray };
// Strip a quoted string value: "foo" → foo, 'foo' → foo.
// Returns the unquoted value or the input unchanged if not quoted.
std::string unquote_toml_string(const std::string& val)
{
if (val.size() >= 2 && ((val.front() == '"' && val.back() == '"') || (val.front() == '\'' && val.back() == '\'')))
return val.substr(1, val.size() - 2);
return val;
}
// Split a TOML inline array: ["a", "b"] → {"a", "b"}.
// Handles trailing commas and single-line format.
std::vector<std::string> parse_toml_inline_array(const std::string& val)
{
std::vector<std::string> result;
std::string inner = val;
// Strip outer brackets.
if (!inner.empty() && inner.front() == '[')
inner.erase(0, 1);
if (!inner.empty() && inner.back() == ']')
inner.pop_back();
// Simple split by comma, strip quotes and whitespace.
std::istringstream ss(inner);
std::string item;
while (std::getline(ss, item, ',')) {
// Trim whitespace.
size_t s = 0, e = item.size();
while (s < e && (item[s] == ' ' || item[s] == '\t')) ++s;
while (e > s && (item[e - 1] == ' ' || item[e - 1] == '\t')) --e;
item = item.substr(s, e - s);
if (!item.empty())
result.push_back(unquote_toml_string(item));
}
return result;
}
// Parse PEP 723 TOML subset for dependencies, requires-python, and
// [tool.orcaslicer.plugin] identity fields.
//
// requires-python = ">=3.12"
// dependencies = ["pkg>=1.0", ]
//
// [tool.orcaslicer.plugin]
// id = "my-plugin"
// name = "My Plugin"
// description = "Does things."
// author = "Author"
// version = "1.0.0"
//
// Returns false only on parse errors; missing block is not an error.
bool parse_pep723_toml(const std::string& toml_content,
std::vector<std::string>& out_deps,
std::string& out_requires_python,
std::string& out_name,
std::string& out_description,
std::string& out_author,
std::string& out_version,
std::string& error)
{
out_deps.clear();
out_requires_python.clear();
out_name.clear();
out_description.clear();
out_author.clear();
out_version.clear();
TomlSection section = TomlSection::Root;
std::istringstream stream(toml_content);
std::string line;
while (std::getline(stream, line)) {
// Trim leading/trailing whitespace.
size_t start = 0;
while (start < line.size() && (line[start] == ' ' || line[start] == '\t'))
++start;
size_t end = line.size();
while (end > start && (line[end - 1] == ' ' || line[end - 1] == '\t'))
--end;
std::string trimmed = line.substr(start, end - start);
if (trimmed.empty() || trimmed[0] == '#')
continue;
// TOML section header.
if (trimmed[0] == '[') {
if (trimmed == "[tool.orcaslicer.plugin]") {
section = TomlSection::OrcaPlugin;
} else {
section = TomlSection::Root; // Unknown section — skip.
}
continue;
}
if (section == TomlSection::InDepsArray) {
if (trimmed == "]") {
section = TomlSection::Root;
continue;
}
std::string val = trimmed;
if (!val.empty() && val.back() == ',')
val.pop_back();
val = unquote_toml_string(val);
if (!val.empty())
out_deps.push_back(val);
continue;
}
// Look for key = value.
size_t eq = trimmed.find('=');
if (eq == std::string::npos)
continue;
std::string key = trimmed.substr(0, eq);
while (!key.empty() && (key.back() == ' ' || key.back() == '\t'))
key.pop_back();
std::string val = trimmed.substr(eq + 1);
while (!val.empty() && (val.front() == ' ' || val.front() == '\t'))
val.erase(0, 1);
// Trim trailing.
while (!val.empty() && (val.back() == ' ' || val.back() == '\t'))
val.pop_back();
if (section == TomlSection::Root) {
if (key == "requires-python") {
out_requires_python = unquote_toml_string(val);
} else if (key == "dependencies") {
if (val == "[") {
section = TomlSection::InDepsArray;
} else {
// Inline array: dependencies = ["a", "b"]
out_deps = parse_toml_inline_array(val);
}
}
} else if (section == TomlSection::OrcaPlugin) {
if (key == "name") out_name = unquote_toml_string(val);
else if (key == "description") out_description = unquote_toml_string(val);
else if (key == "author") out_author = unquote_toml_string(val);
else if (key == "version") out_version = unquote_toml_string(val);
}
}
// Check for unclosed arrays.
if (section == TomlSection::InDepsArray) {
error = "PEP 723 metadata: unclosed dependencies array";
return false;
}
return true;
}
// Normalize a distribution name to a Python import package name.
// Converts hyphens to underscores and lowercases.
std::string normalize_package_name(const std::string& name)
{
std::string result;
result.reserve(name.size());
for (unsigned char ch : name) {
if (ch == '-' || ch == '.')
result += '_';
else
result += static_cast<char>(std::tolower(ch));
}
return result;
}
// Parse METADATA (RFC 822 style) into a flat multimap.
// https://packaging.python.org/en/latest/specifications/core-metadata/
void parse_metadata_rfc822(const std::string& content,
std::string& out_name,
std::string& out_version,
std::string& out_summary,
std::string& out_author,
std::string& out_requires_python,
std::string& out_import_name,
std::vector<std::string>& out_requires_dist,
std::string& error)
{
std::istringstream stream(content);
std::string line;
std::string current_header;
std::string current_value;
auto flush = [&]() {
if (current_header.empty())
return;
std::string lower = current_header;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return std::tolower(c); });
if (lower == "name")
out_name = current_value;
else if (lower == "version")
out_version = current_value;
else if (lower == "summary")
out_summary = current_value;
else if (lower == "author")
out_author = current_value;
else if (lower == "requires-python")
out_requires_python = current_value;
else if (lower == "import-name")
out_import_name = current_value;
else if (lower == "requires-dist")
out_requires_dist.push_back(current_value);
current_header.clear();
current_value.clear();
};
while (std::getline(stream, line)) {
// Blank line after headers marks the start of the body.
if (line.empty() && current_header.empty())
continue;
if (line.empty()) {
flush();
// Remaining content is the body (description); stop parsing headers.
break;
}
// Continuation line.
if (line[0] == ' ' || line[0] == '\t') {
if (!current_value.empty())
current_value += '\n';
size_t pos = line.find_first_not_of(" \t");
current_value += (pos != std::string::npos) ? line.substr(pos) : "";
continue;
}
flush();
size_t colon = line.find(':');
if (colon == std::string::npos)
continue;
current_header = line.substr(0, colon);
size_t val_start = colon + 1;
while (val_start < line.size() && (line[val_start] == ' ' || line[val_start] == '\t'))
++val_start;
current_value = line.substr(val_start);
}
flush();
}
} // namespace
bool is_ignored_plugin_directory(const boost::filesystem::path& path)
{
const std::string name = path.filename().string();
return name.empty() || name[0] == '.' || name.rfind("__", 0) == 0 || name == PLUGIN_SUBSCRIBED_DIR;
}
bool is_safe_relative_path(const boost::filesystem::path& path)
{
if (path.empty() || path.is_absolute() || path.has_root_directory() || path.has_root_name())
return false;
for (const auto& part : path) {
const std::string token = part.string();
if (token == "..")
return false;
}
return true;
}
bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boost::filesystem::path& destination, std::string& error)
{
namespace fs = boost::filesystem;
boost::system::error_code ec;
fs::create_directories(destination, ec);
if (ec) {
error = "Failed to create plugin staging directory: " + ec.message();
return false;
}
ZipReaderGuard reader;
if (!open_zip_reader(&reader.archive, zip_path.string())) {
error = "Failed to open plugin zip: " + MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
return false;
}
reader.opened = true;
std::unordered_set<std::string> extracted_entries;
const mz_uint num_entries = mz_zip_reader_get_num_files(&reader.archive);
mz_zip_archive_file_stat stat;
for (mz_uint i = 0; i < num_entries; ++i) {
if (!mz_zip_reader_file_stat(&reader.archive, i, &stat)) {
error = "Failed to read plugin zip entry metadata: " + MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
return false;
}
std::string entry_name = normalize_zip_entry_name(zip_entry_name(reader.archive, stat));
if (entry_name.empty())
continue;
if (entry_name.find(':') != std::string::npos) {
error = "Plugin zip entry contains an invalid path: " + entry_name;
return false;
}
const fs::path relative_path(entry_name);
if (!is_safe_archive_entry_path(relative_path)) {
error = "Plugin zip entry escapes the plugin package: " + entry_name;
return false;
}
const std::string relative_key = relative_path.generic_string();
if (!extracted_entries.insert(relative_key).second) {
error = "Plugin zip contains duplicate entry: " + relative_key;
return false;
}
const fs::path output_path = destination / relative_path;
if (stat.m_is_directory || mz_zip_reader_is_file_a_directory(&reader.archive, stat.m_file_index)) {
fs::create_directories(output_path, ec);
if (ec) {
error = "Failed to create plugin zip directory " + output_path.string() + ": " + ec.message();
return false;
}
continue;
}
fs::create_directories(output_path.parent_path(), ec);
if (ec) {
error = "Failed to create plugin zip parent directory " + output_path.parent_path().string() + ": " + ec.message();
return false;
}
if (fs::exists(output_path, ec) && fs::is_directory(output_path, ec)) {
error = "Plugin zip file conflicts with an existing directory: " + output_path.string();
return false;
}
const std::string encoded_output_path = encode_path(output_path.string().c_str());
mz_bool extracted = mz_zip_reader_extract_to_file(&reader.archive, stat.m_file_index, encoded_output_path.c_str(), 0);
#ifdef WIN32
if (!extracted) {
const std::wstring wide_output_path = boost::locale::conv::utf_to_utf<wchar_t>(output_path.generic_string());
extracted = mz_zip_reader_extract_to_file_w(&reader.archive, stat.m_file_index, wide_output_path.c_str(), 0);
}
#endif
if (!extracted) {
error = "Failed to extract plugin zip entry " + relative_key + ": " +
MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
return false;
}
}
return true;
}
void read_install_state(const boost::filesystem::path& plugin_dir, PluginDescriptor& entry)
{
namespace fs = boost::filesystem;
const fs::path sidecar_path = plugin_dir / ".install_state.json";
if (!fs::exists(sidecar_path) || !fs::is_regular_file(sidecar_path))
return;
boost::nowide::ifstream f(sidecar_path.string());
if (!f)
return;
try {
nlohmann::json state = nlohmann::json::parse(f, nullptr, false, true);
if (state.is_discarded() || !state.is_object())
return;
// 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
// cloud), so it is not read from the sidecar. installed_version is the source of
// truth for a cloud plugin's installed version: it records the version fetched from
// the cloud at install time, independent of the (possibly stale) manifest/PEP723
// header that scan_directory parses into entry.version.
if (state.contains("installed_version") && state["installed_version"].is_string())
entry.installed_version = state["installed_version"].get<std::string>();
if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string()) {
const std::string cloud_uuid = state["cloud_uuid"].get<std::string>();
if (!cloud_uuid.empty())
entry.cloud = CloudPluginState{cloud_uuid, true, false, false};
}
} catch (...) {}
}
bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstallState& out)
{
namespace fs = boost::filesystem;
const fs::path sidecar_path = plugin_dir / ".install_state.json";
if (!fs::exists(sidecar_path) || !fs::is_regular_file(sidecar_path))
return false;
boost::nowide::ifstream f(sidecar_path.string());
if (!f)
return false;
try {
nlohmann::json state = nlohmann::json::parse(f, nullptr, false, true);
if (state.is_discarded() || !state.is_object())
return false;
PluginInstallState parsed;
if (state.contains("installed_from") && state["installed_from"].is_string())
parsed.installed_from = state["installed_from"].get<std::string>();
if (state.contains("installed_version") && state["installed_version"].is_string())
parsed.installed_version = state["installed_version"].get<std::string>();
if (state.contains("plugin_name") && state["plugin_name"].is_string())
parsed.plugin_name = state["plugin_name"].get<std::string>();
if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string())
parsed.cloud_uuid = state["cloud_uuid"].get<std::string>();
if (state.contains("enabled") && state["enabled"].is_boolean())
parsed.enabled = state["enabled"].get<bool>();
// capabilities is a JSON array of single-key objects {<cap_name>: <bool>}.
if (state.contains("capabilities") && state["capabilities"].is_array()) {
for (const auto& item : state["capabilities"]) {
if (!item.is_object())
continue;
for (auto it = item.begin(); it != item.end(); ++it) {
if (it.value().is_boolean())
parsed.capabilities.emplace_back(it.key(), it.value().get<bool>());
}
}
}
out = std::move(parsed);
return true;
} catch (...) {
return false;
}
}
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state)
{
namespace fs = boost::filesystem;
const fs::path sidecar_path = plugin_dir / ".install_state.json";
nlohmann::json json;
json["installed_from"] = state.installed_from;
json["installed_version"] = state.installed_version;
json["plugin_name"] = state.plugin_name;
json["enabled"] = state.enabled;
if (!state.cloud_uuid.empty())
json["cloud_uuid"] = state.cloud_uuid;
nlohmann::json capabilities = nlohmann::json::array();
for (const auto& [name, enabled] : state.capabilities)
capabilities.push_back(nlohmann::json{{name, enabled}});
json["capabilities"] = std::move(capabilities);
boost::nowide::ofstream f(sidecar_path.string());
if (!f)
return false;
f << json.dump(2);
return static_cast<bool>(f);
}
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry, bool enabled,
const std::vector<std::pair<std::string, bool>>& capabilities)
{
PluginInstallState state;
state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local";
// Prefer the descriptor's recorded installed_version (the version fetched from the cloud
// at install time, preserved across sidecar re-writes) so a stale manifest/PEP723 header
// never overwrites the source-of-truth version. Fall back to the manifest version for
// first-time/local installs where installed_version is not yet populated.
state.installed_version = !entry.installed_version.empty() ? entry.installed_version : entry.version;
state.plugin_name = entry.name;
state.cloud_uuid = entry.cloud_uuid();
state.enabled = enabled;
state.capabilities = capabilities;
return write_install_state(plugin_dir, state);
}
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry)
{
return write_install_state(plugin_dir, entry, true, {});
}
bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error)
{
namespace fs = boost::filesystem;
if (!fs::exists(py_path) || !fs::is_regular_file(py_path)) {
error = "Python plugin file does not exist: " + py_path.string();
return false;
}
boost::nowide::ifstream f(py_path.string());
if (!f) {
error = "Failed to open Python plugin file: " + py_path.string();
return false;
}
// Scan for PEP 723 inline script metadata block.
// The block is delimited by:
// # /// script
// # <toml content>
// # ///
std::string pep723_content;
bool in_block = false;
std::string line;
while (std::getline(f, line)) {
// Strip trailing carriage return (Windows line endings).
if (!line.empty() && line.back() == '\r')
line.pop_back();
if (!in_block) {
// Look for opening delimiter.
if (line == "# /// script")
in_block = true;
continue;
}
if (line == "# ///") {
in_block = false;
continue;
}
// Extract TOML content from the comment line.
// Lines must start with "# " or "#\t" per PEP 723.
if (line.size() >= 2 && line[0] == '#' && (line[1] == ' ' || line[1] == '\t'))
pep723_content += line.substr(2) + "\n";
else if (line == "#")
pep723_content += "\n";
// If line doesn't start with "# ", it's still part of the block content
// but we skip it as it doesn't follow the spec.
}
if (!pep723_content.empty()) {
std::string pep723_error;
std::string requires_python;
std::string pep_name, pep_desc, pep_author, pep_version;
if (!parse_pep723_toml(pep723_content,
descriptor.dependencies,
requires_python,
pep_name,
pep_desc,
pep_author,
pep_version,
pep723_error)) {
error = "Failed to parse PEP 723 metadata: " + pep723_error;
return false;
}
// requires-python is stored but not validated against the bundled Python here.
(void) requires_python;
// Populate identity fields from the PEP 723 [tool.orcaslicer.plugin] section.
// Cloud metadata overrides these when available; they serve as the local
// source of truth for side-loaded .py plugins and as fallback values.
if (!pep_name.empty()) descriptor.name = sanitize_plugin_name(pep_name);
if (!pep_desc.empty()) descriptor.description = pep_desc;
if (!pep_author.empty()) descriptor.author = pep_author;
if (!pep_version.empty()) descriptor.version = pep_version;
}
// Validate that required identity fields are present (either from PEP 723 or
// from cloud metadata already set on the manifest by the caller).
// Validation is deferred to the install/discovery layer so cloud metadata
// can fill in gaps.
return true;
}
bool read_wheel_plugin_metadata(const boost::filesystem::path& whl_path, PluginDescriptor& descriptor, std::string& error)
{
namespace fs = boost::filesystem;
if (!fs::exists(whl_path) || !fs::is_regular_file(whl_path)) {
error = "Wheel plugin file does not exist: " + whl_path.string();
return false;
}
ZipReaderGuard reader;
if (!open_zip_reader(&reader.archive, whl_path.string())) {
error = "Failed to open wheel as zip: " + MZ_Archive::get_errorstr(mz_zip_get_last_error(&reader.archive));
return false;
}
reader.opened = true;
// Find the single .dist-info directory.
// Scan ALL entries, not just directory entries — some zip writers omit
// explicit directory entries. normalize_zip_entry_name strips trailing
// slashes, so we match ".dist-info" as the last path component of a
// directory entry, and ".dist-info/" embedded in a file path.
const mz_uint num_entries = mz_zip_reader_get_num_files(&reader.archive);
std::string dist_info_dir;
mz_zip_archive_file_stat stat;
for (mz_uint i = 0; i < num_entries; ++i) {
if (!mz_zip_reader_file_stat(&reader.archive, i, &stat))
continue;
std::string entry_name = normalize_zip_entry_name(zip_entry_name(reader.archive, stat));
if (entry_name.empty())
continue;
// Find .dist-info as a path component.
size_t pos = entry_name.find(".dist-info");
if (pos == std::string::npos)
continue;
std::string candidate;
if (pos + 10 == entry_name.size()) {
// Directory entry itself (trailing / stripped by normalize).
candidate = entry_name + "/";
} else if (pos + 10 < entry_name.size() && entry_name[pos + 10] == '/') {
// File inside .dist-info/: name.dist-info/METADATA
candidate = entry_name.substr(0, pos + 11); // include trailing /
} else {
continue; // .dist-info mid-name, not a path component.
}
if (!dist_info_dir.empty() && candidate != dist_info_dir) {
error = "Wheel contains multiple .dist-info directories: " + dist_info_dir + " and " + candidate;
return false;
}
dist_info_dir = candidate;
}
if (dist_info_dir.empty()) {
error = "Wheel does not contain a .dist-info directory";
return false;
}
// Read METADATA.
const std::string metadata_path = dist_info_dir + "METADATA";
std::string meta_content;
if (!read_zip_text_file(reader.archive, metadata_path.c_str(), meta_content, error))
return false;
std::string meta_name, meta_version, meta_summary, meta_author, meta_requires_python, meta_import_name;
std::vector<std::string> requires_dist;
std::string meta_error;
parse_metadata_rfc822(meta_content, meta_name, meta_version, meta_summary, meta_author,
meta_requires_python, meta_import_name, requires_dist, meta_error);
if (meta_name.empty()) {
error = "Wheel METADATA missing required Name field";
return false;
}
if (meta_version.empty()) {
error = "Wheel METADATA missing required Version field";
return false;
}
// Read WHEEL (verify existence and Wheel-Version).
const std::string wheel_path = dist_info_dir + "WHEEL";
std::string wheel_content;
if (!read_zip_text_file(reader.archive, wheel_path.c_str(), wheel_content, error))
return false;
// Verify there's at least a Wheel-Version header line.
if (wheel_content.find("Wheel-Version:") == std::string::npos) {
error = "Wheel WHEEL file missing Wheel-Version header";
return false;
}
// Parse and validate wheel platform tags.
{
std::vector<std::string> wheel_tags;
std::istringstream wstream(wheel_content);
std::string wline;
while (std::getline(wstream, wline)) {
while (!wline.empty() && (wline.back() == '\r' || wline.back() == '\n'))
wline.pop_back();
if (wline.rfind("Tag:", 0) == 0) {
std::string tag = wline.substr(4);
size_t s = 0, e = tag.size();
while (s < e && (tag[s] == ' ' || tag[s] == '\t')) ++s;
while (e > s && (tag[e - 1] == ' ' || tag[e - 1] == '\t')) --e;
wheel_tags.push_back(tag.substr(s, e - s));
}
}
if (!wheel_tags.empty()) {
bool compatible = false;
const std::string abi_tag = PythonInterpreter::python_abi_tag();
for (const auto& tag : wheel_tags) {
// Pure Python wheel: py3-none-any or cp312-none-any
if (tag.find("-none-any") != std::string::npos) {
compatible = true;
break;
}
// Platform-specific: check ABI tag matches.
if (tag.find(abi_tag) == 0) {
// Accept if the platform tag matches the current OS.
#ifdef _WIN32
if (tag.find("-win") != std::string::npos)
compatible = true;
#elif __APPLE__
if (tag.find("-macosx") != std::string::npos)
compatible = true;
#else
if (tag.find("-linux") != std::string::npos || tag.find("-manylinux") != std::string::npos)
compatible = true;
#endif
}
}
if (!compatible) {
error = "Wheel is incompatible with this platform. Tags: ";
for (size_t i = 0; i < wheel_tags.size(); ++i) {
if (i > 0) error += ", ";
error += wheel_tags[i];
}
error += "; expected ABI: " + abi_tag;
return false;
}
}
}
// Read RECORD (verify existence).
const std::string record_path = dist_info_dir + "RECORD";
std::string record_content;
if (!read_zip_text_file(reader.archive, record_path.c_str(), record_content, error))
return false;
if (record_content.empty()) {
error = "Wheel RECORD file is empty";
return false;
}
// Parse top_level.txt if present.
std::string top_level;
const std::string top_level_path = dist_info_dir + "top_level.txt";
std::string top_level_content;
if (read_zip_text_file(reader.archive, top_level_path.c_str(), top_level_content, error)) {
// top_level.txt contains one package name per line.
std::istringstream tl_stream(top_level_content);
std::string tl_line;
std::vector<std::string> top_levels;
while (std::getline(tl_stream, tl_line)) {
while (!tl_line.empty() && (tl_line.back() == '\r' || tl_line.back() == '\n'))
tl_line.pop_back();
if (!tl_line.empty())
top_levels.push_back(tl_line);
}
if (top_levels.size() == 1)
top_level = top_levels[0];
else if (top_levels.size() > 1) {
// Ambiguous: multiple top-level packages. Fall through to Name-based fallback.
}
// Zero entries: leave top_level empty.
}
// If top_level.txt is not found, that's OK — it's optional per the wheel spec.
// Determine the entry package in priority order.
// 1. Cloud/catalog metadata — handled by caller, not here.
// 2. Core Metadata Import-Name.
// 3. top_level.txt if unambiguous.
// 4. Normalized Name as fallback.
if (!meta_import_name.empty()) {
descriptor.entry_package = meta_import_name;
} else if (!top_level.empty()) {
descriptor.entry_package = top_level;
} else {
descriptor.entry_package = normalize_package_name(meta_name);
}
descriptor.dependencies = std::move(requires_dist);
// Populate local identity fallbacks from wheel metadata.
// Cloud metadata will override these when available.
descriptor.name = sanitize_plugin_name(meta_name);
descriptor.version = meta_version;
descriptor.description = meta_summary;
descriptor.author = meta_author;
return true;
}
boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error)
{
namespace fs = boost::filesystem;
if (!fs::exists(plugin_dir) || !fs::is_directory(plugin_dir)) {
error = "Plugin directory does not exist: " + plugin_dir.string();
return {};
}
fs::path py_entry;
fs::path whl_entry;
for (fs::directory_iterator it(plugin_dir); it != fs::directory_iterator(); ++it) {
if (is_ignored_plugin_directory(it->path()))
continue;
if (!fs::is_regular_file(it->status()))
continue;
const fs::path ext = it->path().extension();
if (ext == ".py") {
if (!py_entry.empty()) {
error = "Plugin directory contains multiple .py files: " + py_entry.filename().string() +
" and " + it->path().filename().string();
return {};
}
py_entry = it->path();
} else if (ext == ".whl") {
if (!whl_entry.empty()) {
error = "Plugin directory contains multiple .whl files: " + whl_entry.filename().string() +
" and " + it->path().filename().string();
return {};
}
whl_entry = it->path();
}
}
if (!py_entry.empty() && !whl_entry.empty()) {
error = "Plugin directory contains both .py and .whl entry files";
return {};
}
if (!py_entry.empty())
return py_entry;
if (!whl_entry.empty())
return whl_entry;
error = "Plugin directory does not contain a .py or .whl entry file";
return {};
}
} // namespace Slic3r
+61
View File
@@ -0,0 +1,61 @@
#ifndef slic3r_PythonFileUtils_hpp_
#define slic3r_PythonFileUtils_hpp_
#include <boost/filesystem/path.hpp>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
struct PluginDescriptor;
// Persisted per-plugin install/auto-load state, stored in the .install_state.json sidecar.
// This replaces app_config as the source of truth for auto-load and capability enable state.
struct PluginInstallState {
std::string installed_from; // "local" | "cloud"
std::string installed_version;
std::string plugin_name;
std::string cloud_uuid; // empty for local
bool enabled = true;
std::vector<std::pair<std::string, bool>> capabilities; // name -> enabled, ordered
};
bool is_ignored_plugin_directory(const boost::filesystem::path& path);
bool is_safe_relative_path(const boost::filesystem::path& path);
bool is_valid_plugin_id(const std::string& id);
bool extract_zip_to_directory(const boost::filesystem::path& zip_path, const boost::filesystem::path& destination, std::string& error);
// Read PEP 723 inline script metadata from a .py plugin file.
// Populates metadata.dependencies and local identity fallbacks.
// Returns true on success (including when no PEP 723 block is found — deps will be empty).
bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error);
// Read wheel metadata from a .whl plugin file (zip archive).
// Reads METADATA, WHEEL, RECORD, and top_level.txt from the .dist-info directory.
// Populates metadata.entry_package, metadata.dependencies, and local identity fallbacks.
// Returns true on success.
bool read_wheel_plugin_metadata(const boost::filesystem::path& whl_path, PluginDescriptor& descriptor, std::string& error);
// Find the single plugin entry file (.py or .whl) in a directory.
// Ignores __whl_extracted__ and hidden files/dirs.
// Returns the path to the entry file, or an empty path with error set if zero or multiple candidates.
boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::path& plugin_dir, std::string& error);
// Canonical writer: emits the .install_state.json schema for the given state.
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginInstallState& state);
// Builds a PluginInstallState from the descriptor and delegates to the canonical writer.
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry, bool enabled,
const std::vector<std::pair<std::string, bool>>& capabilities);
// 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);
// 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);
} // namespace Slic3r
#endif // slic3r_PythonFileUtils_hpp_
+996
View File
@@ -0,0 +1,996 @@
#include "PythonInterpreter.hpp"
#include "GeneratedConfig.hpp"
#include "libslic3r/Utils.hpp"
#include "PluginAuditManager.hpp"
#include <boost/filesystem/path.hpp>
#include <pytypedefs.h>
#include "PythonFileUtils.hpp"
#include <pybind11/embed.h>
#include <boost/log/trivial.hpp>
#include <boost/filesystem.hpp>
#include <boost/dll/runtime_symbol_info.hpp>
#include <boost/nowide/convert.hpp>
#include <cstring>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <vector>
namespace Slic3r {
void log_python_exception_keep(pybind11::error_already_set& err)
{
// The GIL may already be released here: the macro's gil_scoped_acquire is a
// local destroyed by stack unwinding before this catch runs. Touching Python
// state below needs the GIL. PyGILState_Ensure is reentrant — harmless if held.
PythonGILState gil;
// Non-destructive: print the traceback to sys.stderr (tee'd to the session log)
// WITHOUT consuming err, so the caller can rethrow it intact. For example, downstream C++
// catchers can still read err.what() for the user-facing dialog. We must NOT use
// restore()+PyErr_Print() here as those empty err.
try {
namespace py = pybind11;
py::module_ tb = py::module_::import("traceback");
py::module_ sys = py::module_::import("sys");
tb.attr("print_exception")(err.type(), err.value(), err.trace(), py::none(), sys.attr("stderr"));
} catch (...) {
// Fallback: at least get the formatted message out. err.what() includes the
// traceback in recent pybind11 and does not consume err.
try {
pybind11::module_::import("sys").attr("stderr").attr("write")(std::string(err.what()) + "\n");
} catch (...) {
}
}
}
namespace {
std::string format_python_error(PyObject* ptype, PyObject* pvalue, PyObject* ptraceback)
{
std::string result;
if (ptype) {
PyObject* type_name = PyObject_GetAttrString(ptype, "__name__");
if (type_name) {
const char* s = PyUnicode_AsUTF8(type_name);
if (s) result += s;
Py_DECREF(type_name);
}
}
if (pvalue) {
if (!result.empty()) result += ": ";
PyObject* s = PyObject_Str(pvalue);
if (s) {
const char* cstr = PyUnicode_AsUTF8(s);
if (cstr) result += cstr;
Py_DECREF(s);
}
}
if (ptraceback) {
result += "\nTraceback (most recent call last):";
PyTracebackObject* tb = reinterpret_cast<PyTracebackObject*>(ptraceback);
while (tb) {
PyFrameObject* frame = tb->tb_frame;
int line = PyFrame_GetLineNumber(frame);
PyCodeObject* code = PyFrame_GetCode(frame); // returns a NEW strong reference; must be released
const char* filename = code ? PyUnicode_AsUTF8(code->co_filename) : nullptr;
const char* funcname = code ? PyUnicode_AsUTF8(code->co_name) : nullptr;
result += "\n File \"" + std::string(filename ? filename : "?") +
"\", line " + std::to_string(line) +
", in " + std::string(funcname ? funcname : "?");
Py_XDECREF(code);
tb = tb->tb_next;
}
}
return result;
}
#ifdef _WIN32
constexpr const char* PYTHON_DLL = "python312.dll";
constexpr const char* PYTHON_DEBUG_DLL = "python312_d.dll";
#else
constexpr const char* PYTHON_STDLIB_DIR = "python3.12";
constexpr const char* PYTHON_EXECUTABLE = "python3.12";
#endif
std::string executable_name(const char* base)
{
#ifdef _WIN32
return std::string(base) + ".exe";
#else
return base;
#endif
}
bool add_sys_path_entry(const boost::filesystem::path& path, std::string& error)
{
PyObject* sys_path = PySys_GetObject("path");
if (!sys_path || !PyList_Check(sys_path)) {
error = "Python sys.path is not available";
return false;
}
const std::string path_str = path.string();
PyObjectPtr py_path(PyUnicode_DecodeFSDefault(path_str.c_str()));
if (!py_path) {
error = "Failed to decode path for Python sys.path: " + path_str;
PyErr_Clear();
return false;
}
const int contains = PySequence_Contains(sys_path, py_path.get());
if (contains == 1)
return true;
if (contains < 0)
PyErr_Clear();
if (PyList_Insert(sys_path, 0, py_path.get()) != 0) {
error = "Failed to add path to Python sys.path: " + path_str;
PyErr_Clear();
return false;
}
return true;
}
void log_python_stream(PyObject* sys, const char* name)
{
PyObject* stream = PyObject_GetAttrString(sys, name);
if (!stream) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(info) << "Python shutdown: sys." << name << " is not set";
return;
}
PyObject* original = nullptr;
std::string original_name = std::string("__") + name + "__";
original = PyObject_GetAttrString(sys, original_name.c_str());
if (!original)
PyErr_Clear();
BOOST_LOG_TRIVIAL(info) << "Python shutdown: sys." << name << " type=" << Py_TYPE(stream)->tp_name << " ptr=" << stream
<< " original_ptr=" << original << " is_original=" << (stream == original);
Py_XDECREF(original);
Py_DECREF(stream);
}
void restore_python_stream(PyObject* sys, const char* name)
{
std::string original_name = std::string("__") + name + "__";
PyObject* original = PyObject_GetAttrString(sys, original_name.c_str());
if (!original) {
PyErr_Clear();
original = Py_NewRef(Py_None);
}
if (PyObject_SetAttrString(sys, name, original) < 0) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(warning) << "Python shutdown: failed to restore sys." << name;
} else {
BOOST_LOG_TRIVIAL(info) << "Python shutdown: restored sys." << name << " to type=" << Py_TYPE(original)->tp_name
<< " ptr=" << original;
}
Py_DECREF(original);
}
// Tee Python sys.stderr to <data_dir>/log/python_*.log so plugin errors are
// persisted. Uncaught exceptions in plugin-spawned threads never cross the
// pybind11 boundary back into C++ — CPython's default threading.excepthook
// prints them to sys.stderr and lets the thread die — so capturing stderr is
// the one place all of them can be observed.
void install_python_stderr_redirect()
{
// Mirror the session filename convention from GUI_App: python_<weekday>_<mon>_<day>_<HH>_<MM>_<SS>_<pid>.log
std::time_t t = std::time(nullptr);
std::tm* now = std::localtime(&t);
std::ostringstream name;
name << std::put_time(now, "python_%a_%b_%d_%H_%M_%S_") << get_current_pid() << ".log";
const std::string log_path = (boost::filesystem::path(data_dir()) / "log" / name.str()).generic_string();
const std::string redirect_script =
"import io, os, sys, threading\n"
"_ORCA_PYTHON_LOG = \"" + log_path + "\"\n"
+ std::string(R"REDIRECT_SCRIPT(
class _OrcaTeeStderr(io.TextIOBase):
def __init__(self, original, path):
self._original = original
self._path = path
self._lock = threading.Lock()
def writable(self):
return True
def write(self, s):
try:
if self._original is not None:
self._original.write(s)
except Exception:
pass
try:
with self._lock, open(self._path, "a", encoding="utf-8", errors="replace") as f:
f.write(s)
except Exception:
pass
return len(s)
def flush(self):
try:
if self._original is not None:
self._original.flush()
except Exception:
pass
os.makedirs(os.path.dirname(_ORCA_PYTHON_LOG), exist_ok=True)
sys.stderr = _OrcaTeeStderr(sys.__stderr__, _ORCA_PYTHON_LOG)
del _OrcaTeeStderr
)REDIRECT_SCRIPT");
if (PyRun_SimpleString(redirect_script.c_str()) != 0) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(warning) << "Failed to install Python stderr redirect to " << log_path;
} else {
BOOST_LOG_TRIVIAL(info) << "Python stderr redirected to " << log_path;
}
}
void log_and_restore_python_stdio()
{
PyObject* sys = PyImport_ImportModule("sys");
if (!sys) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(warning) << "Python shutdown: failed to import sys for stdio diagnostics";
return;
}
log_python_stream(sys, "stdout");
log_python_stream(sys, "stderr");
restore_python_stream(sys, "stdout");
restore_python_stream(sys, "stderr");
Py_DECREF(sys);
}
bool valid_python_home(const boost::filesystem::path& candidate)
{
#ifdef _WIN32
return boost::filesystem::exists(candidate / "Lib" / "encodings") &&
(boost::filesystem::exists(candidate / PYTHON_DLL) || boost::filesystem::exists(candidate / PYTHON_DEBUG_DLL));
#else
return boost::filesystem::exists(candidate / "lib" / PYTHON_STDLIB_DIR / "encodings");
#endif
}
boost::filesystem::path find_bundled_python_home()
{
namespace fs = boost::filesystem;
#ifdef __APPLE__
fs::path bundle_python = fs::path(resources_dir()).parent_path() / "MacOS" / "python";
if (valid_python_home(bundle_python))
return bundle_python;
#elif defined(_WIN32)
fs::path exe_python = boost::dll::program_location().parent_path() / "python";
if (valid_python_home(exe_python))
return exe_python;
#else
fs::path linux_python = fs::path(resources_dir()).parent_path() / "lib" / "python";
if (valid_python_home(linux_python))
return linux_python;
#endif
fs::path configured_python = ORCA_BUNDLED_PYTHON_ROOT;
if (!configured_python.empty() && valid_python_home(configured_python))
return configured_python;
const char* prefix_path = std::getenv("CMAKE_PREFIX_PATH");
if (prefix_path && std::strlen(prefix_path) > 0) {
fs::path libpython = fs::path(prefix_path) / "libpython";
if (valid_python_home(libpython))
return libpython;
}
fs::path res_python = fs::path(resources_dir()) / "python";
if (valid_python_home(res_python))
return res_python;
#ifndef _WIN32
fs::path data_python = fs::path(data_dir()) / "python";
if (valid_python_home(data_python))
return data_python;
#endif
return {};
}
boost::filesystem::path find_python_executable(const boost::filesystem::path& python_home)
{
namespace fs = boost::filesystem;
#ifdef _WIN32
const std::vector<fs::path> candidates = {
python_home / "python.exe",
python_home / "python_d.exe",
};
#else
const std::vector<fs::path> candidates = {
python_home / "bin" / PYTHON_EXECUTABLE,
python_home / "bin" / "python3",
python_home / "bin" / "python",
};
#endif
for (const fs::path& candidate : candidates) {
if (fs::exists(candidate) && fs::is_regular_file(candidate))
return candidate;
}
return {};
}
} // namespace
std::string PythonInterpreter::python_abi_tag()
{
return std::string("cp") + std::to_string(PY_MAJOR_VERSION) + std::to_string(PY_MINOR_VERSION);
}
PythonInterpreter& PythonInterpreter::instance()
{
static PythonInterpreter inst;
return inst;
}
std::string PythonInterpreter::shared_packages_dir()
{
return (boost::filesystem::path(data_dir()) / "python" / "packages" / PythonInterpreter::python_abi_tag()).string();
}
std::string PythonInterpreter::bundled_python_executable()
{
const boost::filesystem::path python_home = find_bundled_python_home();
if (python_home.empty())
return {};
const boost::filesystem::path executable = find_python_executable(python_home);
return executable.empty() ? std::string{} : executable.string();
}
std::string PythonInterpreter::bundled_uv_path()
{
namespace fs = boost::filesystem;
const fs::path configured_uv = ORCA_BUNDLED_UV_EXECUTABLE;
if (!configured_uv.empty() && fs::exists(configured_uv) && fs::is_regular_file(configured_uv))
return configured_uv.string();
// <binary dir>/tools/uv covers the macOS bundle (Contents/MacOS/tools/uv)
// and build-tree runs; <resources>/tools/uv covers the install() and
// AppImage layouts.
const std::string uv_exe = executable_name("uv");
const std::vector<fs::path> candidates = {
fs::path(resources_dir()) / "tools" / "uv" / uv_exe,
boost::dll::program_location().parent_path() / "tools" / "uv" / uv_exe,
};
for (const fs::path& candidate : candidates) {
if (fs::exists(candidate) && fs::is_regular_file(candidate))
return candidate.string();
}
return {};
}
bool PythonInterpreter::initialize()
{
if (m_initialized) {
return true;
}
m_last_error.clear();
try {
// Set Python home to the bundled Python installation
// This is critical for finding the standard library (encodings module, etc.)
namespace fs = boost::filesystem;
std::string python_home;
const auto valid_python_home = [](const fs::path& candidate) {
#ifdef _WIN32
return fs::exists(candidate / "Lib" / "encodings") &&
(fs::exists(candidate / PYTHON_DLL) || fs::exists(candidate / PYTHON_DEBUG_DLL));
#else
return fs::exists(candidate / "lib" / PYTHON_STDLIB_DIR / "encodings");
#endif
};
// Determine Python home based on application structure
// Python is bundled at different locations depending on platform and build type
// Strategy 1: Platform-specific bundled locations (highest priority)
#ifdef __APPLE__
// macOS app bundle: OrcaSlicer.app/Contents/MacOS/python
// (resources_dir is Contents/Resources, so go up and into MacOS)
fs::path bundle_python = fs::path(resources_dir()).parent_path() / "MacOS" / "python";
if (valid_python_home(bundle_python)) {
python_home = bundle_python.string();
BOOST_LOG_TRIVIAL(info) << "Found Python in macOS app bundle: " << python_home;
}
#elif defined(_WIN32)
fs::path exe_python = boost::dll::program_location().parent_path() / "python";
if (valid_python_home(exe_python)) {
python_home = exe_python.string();
BOOST_LOG_TRIVIAL(info) << "Found Python next to Windows executable: " << python_home;
}
#else
// Linux: typically in ../lib or ../share relative to binary
fs::path linux_python = fs::path(resources_dir()).parent_path() / "lib" / "python";
if (valid_python_home(linux_python)) {
python_home = linux_python.string();
BOOST_LOG_TRIVIAL(info) << "Found Python in Linux install: " << python_home;
}
#endif
// Strategy 2: Configured development dependency directory.
if (python_home.empty()) {
fs::path configured_python = ORCA_BUNDLED_PYTHON_ROOT;
if (!configured_python.empty() && valid_python_home(configured_python)) {
python_home = configured_python.string();
BOOST_LOG_TRIVIAL(info) << "Found Python in configured bundled path: " << python_home;
}
}
// Strategy 3: Development build directory from runtime environment.
if (python_home.empty()) {
const char* prefix_path = std::getenv("CMAKE_PREFIX_PATH");
if (prefix_path && std::strlen(prefix_path) > 0) {
fs::path libpython = fs::path(prefix_path) / "libpython";
if (valid_python_home(libpython)) {
python_home = libpython.string();
BOOST_LOG_TRIVIAL(info) << "Found Python in CMAKE_PREFIX_PATH: " << python_home;
}
}
}
// Strategy 3: Check resources directory (alternate bundling location)
if (python_home.empty()) {
fs::path res_python = fs::path(resources_dir()) / "python";
if (valid_python_home(res_python)) {
python_home = res_python.string();
BOOST_LOG_TRIVIAL(info) << "Found Python in resources directory: " << python_home;
}
}
// Strategy 4: Check data_dir (user configuration directory)
#ifndef _WIN32
if (python_home.empty()) {
fs::path data_python = fs::path(data_dir()) / "python";
if (valid_python_home(data_python)) {
python_home = data_python.string();
BOOST_LOG_TRIVIAL(info) << "Found Python in data directory: " << python_home;
}
}
#endif
if (python_home.empty()) {
m_last_error = "Could not locate bundled Python installation";
BOOST_LOG_TRIVIAL(error) << "Could not locate bundled Python installation";
BOOST_LOG_TRIVIAL(error) << "Configured bundled Python root: " << ORCA_BUNDLED_PYTHON_ROOT;
#ifdef _WIN32
BOOST_LOG_TRIVIAL(error) << "Searched next to executable: "
<< (boost::dll::program_location().parent_path() / "python").string();
#endif
BOOST_LOG_TRIVIAL(error) << "Searched in resources_dir: " << resources_dir();
#ifndef _WIN32
BOOST_LOG_TRIVIAL(error) << "Searched in data_dir: " << data_dir();
#endif
BOOST_LOG_TRIVIAL(error) << "CMAKE_PREFIX_PATH: "
<< (std::getenv("CMAKE_PREFIX_PATH") ? std::getenv("CMAKE_PREFIX_PATH") : "not set");
return false;
}
// Verify Python standard library directory exists
#ifdef _WIN32
fs::path python_lib = fs::path(python_home) / "Lib";
#else
fs::path python_lib = fs::path(python_home) / "lib" / PYTHON_STDLIB_DIR;
#endif
if (!fs::exists(python_lib)) {
m_last_error = "Python standard library directory not found at: " + python_lib.string();
BOOST_LOG_TRIVIAL(error) << "Python standard library directory not found at: " << python_lib.string();
BOOST_LOG_TRIVIAL(error) << "Please build Python dependencies or check installation";
return false;
}
BOOST_LOG_TRIVIAL(info) << "Python standard library found at: " << python_lib.string();
#ifdef _WIN32
fs::path python_dll = fs::exists(fs::path(python_home) / PYTHON_DLL) ? fs::path(python_home) / PYTHON_DLL :
fs::path(python_home) / PYTHON_DEBUG_DLL;
if (!fs::exists(python_dll)) {
m_last_error = "Python DLL not found in: " + python_home;
BOOST_LOG_TRIVIAL(error) << "Python DLL not found in: " << python_home;
return false;
}
BOOST_LOG_TRIVIAL(info) << "Python DLL found at: " << python_dll.string();
if (!fs::exists(fs::path(python_home) / "DLLs")) {
BOOST_LOG_TRIVIAL(warning) << "Python DLLs directory not found at: " << (fs::path(python_home) / "DLLs").string();
}
#endif
// Log the exact paths being used for debugging
BOOST_LOG_TRIVIAL(info) << "Setting Python home to: " << python_home;
BOOST_LOG_TRIVIAL(info) << "Python 3.12 stdlib path: " << python_lib.string();
// Verify encodings module exists
fs::path encodings_path = python_lib / "encodings";
if (fs::exists(encodings_path)) {
BOOST_LOG_TRIVIAL(info) << "Encodings module found at: " << encodings_path.string();
} else {
BOOST_LOG_TRIVIAL(warning) << "Encodings module NOT found at: " << encodings_path.string();
}
BOOST_LOG_TRIVIAL(info) << "Using Python 3.12 PyConfig initialization API";
// Set Python home - this is the prefix where Python libraries are located
PyConfig config;
PyConfig_InitPythonConfig(&config);
BOOST_LOG_TRIVIAL(debug) << "Calling PyConfig_SetBytesString with home=" << python_home;
PyStatus status = PyConfig_SetBytesString(&config, &config.home, python_home.c_str());
if (PyStatus_Exception(status)) {
m_last_error = status.err_msg ? status.err_msg : "Failed to set Python home";
BOOST_LOG_TRIVIAL(error) << "Failed to set Python home to: " << python_home << ": " << m_last_error;
PyConfig_Clear(&config);
return false;
}
BOOST_LOG_TRIVIAL(debug) << "Python home set successfully";
// Set program name
status = PyConfig_SetBytesString(&config, &config.program_name, "OrcaSlicer");
if (PyStatus_Exception(status)) {
m_last_error = status.err_msg ? status.err_msg : "Failed to set program name";
BOOST_LOG_TRIVIAL(error) << "Failed to set program name: " << m_last_error;
PyConfig_Clear(&config);
return false;
}
BOOST_LOG_TRIVIAL(debug) << "Program name set successfully";
// Use pybind11's scoped_interpreter which properly initializes
// pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig
// does not set up thread state tracking that pybind11 requires.
BOOST_LOG_TRIVIAL(debug) << "Creating py::scoped_interpreter with PyConfig...";
try {
m_interpreter = std::make_unique<pybind11::scoped_interpreter>(&config);
} catch (const std::exception& ex) {
m_last_error = std::string("Python initialization failed: ") + ex.what();
BOOST_LOG_TRIVIAL(error) << m_last_error;
PyConfig_Clear(&config);
return false;
}
// PyConfig is cleared by pybind11's initialize_interpreter() internally.
BOOST_LOG_TRIVIAL(debug) << "py::scoped_interpreter initialized successfully";
if (!Py_IsInitialized()) {
m_last_error = "Python interpreter not initialized";
BOOST_LOG_TRIVIAL(error) << "Python interpreter not initialized";
return false;
}
// Log Python paths for debugging
PyObject* sys = PyImport_ImportModule("sys");
if (sys) {
PyObject* path = PyObject_GetAttrString(sys, "path");
if (path) {
PyObject* path_str = PyObject_Str(path);
if (path_str) {
const char* path_cstr = PyUnicode_AsUTF8(path_str);
if (path_cstr) {
BOOST_LOG_TRIVIAL(debug) << "Python sys.path: " << path_cstr;
}
Py_DECREF(path_str);
}
Py_DECREF(path);
}
Py_DECREF(sys);
}
BOOST_LOG_TRIVIAL(info) << "Python " << Py_GetVersion() << " initialized successfully";
const fs::path shared_packages = shared_packages_dir();
boost::system::error_code ec;
fs::create_directories(shared_packages, ec);
if (ec) {
BOOST_LOG_TRIVIAL(warning) << "Failed to create Python shared package directory: " << shared_packages.string() << ": "
<< ec.message();
} else {
std::string path_error;
if (add_sys_path_entry(shared_packages, path_error))
BOOST_LOG_TRIVIAL(info) << "Added Python shared package directory to sys.path: " << shared_packages.string();
else
BOOST_LOG_TRIVIAL(warning) << path_error;
}
const std::string uv_path = bundled_uv_path();
if (!uv_path.empty())
BOOST_LOG_TRIVIAL(info) << "Bundled uv executable found at: " << uv_path;
else
BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found";
// Install the CPython audit hook for plugin policy enforcement.
// This is defense-in-depth: it monitors file/subprocess/socket/ctypes
// access from plugin code. It is NOT a full security sandbox.
PluginAuditManager::instance().install_hook();
// Persist Python stderr (plugin tracebacks, including uncaught
// background-thread exceptions) to <data_dir>/log/python_*.log.
install_python_stderr_redirect();
m_initialized = true;
// Release the GIL so other threads can acquire it via PyGILState_Ensure.
// Without this, calls from background threads will block trying to acquire the GIL.
m_main_thread_state = PyEval_SaveThread();
BOOST_LOG_TRIVIAL(debug) << "Main thread released Python GIL after initialization";
return true;
} catch (const std::exception& ex) {
m_last_error = std::string("Exception initializing Python: ") + ex.what();
BOOST_LOG_TRIVIAL(error) << "Exception initializing Python: " << ex.what();
return false;
}
}
PythonInterpreter::~PythonInterpreter() { shutdown(); }
void PythonInterpreter::shutdown()
{
if (!m_initialized)
return;
BOOST_LOG_TRIVIAL(info) << "Python interpreter shutdown enter";
// Reacquire the GIL using the saved thread state before finalizing.
if (m_main_thread_state) {
BOOST_LOG_TRIVIAL(debug) << "Restoring Python main thread state before shutdown";
PyEval_RestoreThread(m_main_thread_state);
m_main_thread_state = nullptr;
}
log_and_restore_python_stdio();
BOOST_LOG_TRIVIAL(info) << "Finalizing Python interpreter";
m_interpreter.reset();
BOOST_LOG_TRIVIAL(info) << "Python interpreter finalized";
m_initialized = false;
}
bool PythonInterpreter::add_sys_path(const std::string& path, std::string& error)
{
if (!m_initialized) {
error = "Python interpreter not initialized";
return false;
}
PythonGILState gil;
PyObject* sys_path = PySys_GetObject("path");
if (!sys_path || !PyList_Check(sys_path)) {
error = "Python sys.path is not available";
return false;
}
PyObjectPtr py_path(PyUnicode_DecodeFSDefault(path.c_str()));
if (!py_path) {
error = "Failed to decode path for Python sys.path: " + path;
PyErr_Clear();
return false;
}
const int contains = PySequence_Contains(sys_path, py_path.get());
if (contains == 1)
return true;
if (contains < 0)
PyErr_Clear();
if (PyList_Insert(sys_path, 0, py_path.get()) != 0) {
error = "Failed to append path to Python sys.path: " + path;
PyErr_Clear();
return false;
}
return true;
}
bool PythonInterpreter::execute_string(const std::string& code, std::string& error)
{
if (!m_initialized) {
error = "Python interpreter not initialized";
return false;
}
PythonGILState gil;
PyObject* main_module = PyImport_AddModule("__main__");
if (!main_module) {
error = "Failed to get __main__ module";
return false;
}
PyObject* global_dict = PyModule_GetDict(main_module);
PyObjectPtr result(PyRun_String(code.c_str(), Py_file_input, global_dict, global_dict));
if (!result) {
PyObject *ptype, *pvalue, *ptraceback;
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
error = format_python_error(ptype, pvalue, ptraceback);
Py_XDECREF(ptype);
Py_XDECREF(pvalue);
Py_XDECREF(ptraceback);
return false;
}
return true;
}
PyObject* PythonInterpreter::load_module_from_file(const std::string& file_path, std::string& error)
{
if (!m_initialized) {
error = "Python interpreter not initialized";
return nullptr;
}
namespace fs = boost::filesystem;
fs::path path(file_path);
if (!fs::exists(path)) {
error = "File does not exist: " + file_path;
return nullptr;
}
PythonGILState gil;
// Add the directory to sys.path
fs::path dir_path = path.parent_path();
std::string module_name = path.stem().string();
PyObjectPtr sys(PyImport_ImportModule("sys"));
if (!sys) {
error = "Failed to import sys module";
return nullptr;
}
PyObject* sys_path = PyObject_GetAttrString(sys.get(), "path");
if (!sys_path) {
error = "Failed to get sys.path";
return nullptr;
}
PyObjectPtr dir_str(PyUnicode_FromString(dir_path.string().c_str()));
if (!dir_str) {
Py_DECREF(sys_path);
error = "Failed to create directory string";
return nullptr;
}
if (PyList_Insert(sys_path, 0, dir_str.get()) < 0) {
Py_DECREF(sys_path);
error = "Failed to add directory to sys.path";
return nullptr;
}
Py_DECREF(sys_path);
// Ensure module is re-imported fresh by removing any cached instance.
if (PyObject* modules = PyImport_GetModuleDict()) {
if (PyDict_GetItemString(modules, module_name.c_str())) {
if (PyDict_DelItemString(modules, module_name.c_str()) != 0) {
PyErr_Clear();
}
}
}
// Import the module
PyObject* module = PyImport_ImportModule(module_name.c_str());
if (!module) {
PyObject *ptype, *pvalue, *ptraceback;
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
error = "Failed to import module: " + format_python_error(ptype, pvalue, ptraceback);
Py_XDECREF(ptype);
Py_XDECREF(pvalue);
Py_XDECREF(ptraceback);
return nullptr;
}
return module;
}
PyObject* PythonInterpreter::load_module_from_directory(const std::string& dir_path, const std::string& pkg_name, std::string& error)
{
if (!m_initialized) {
error = "Python interpreter not initialized";
return nullptr;
}
namespace fs = boost::filesystem;
fs::path dir(dir_path);
if (!fs::exists(dir) || !fs::is_directory(dir)) {
error = "Directory does not exist: " + dir_path;
return nullptr;
}
PythonGILState gil;
PyObjectPtr sys(PyImport_ImportModule("sys"));
if (!sys) {
error = "Failed to import sys module";
return nullptr;
}
PyObject* sys_path = PyObject_GetAttrString(sys.get(), "path");
if (!sys_path) {
error = "Failed to get sys.path";
return nullptr;
}
PyObjectPtr dir_str(PyUnicode_FromString(dir.string().c_str()));
if (!dir_str) {
Py_DECREF(sys_path);
error = "Failed to create directory string";
return nullptr;
}
if (PyList_Insert(sys_path, 0, dir_str.get()) < 0) {
Py_DECREF(sys_path);
error = "Failed to add directory to sys.path";
return nullptr;
}
Py_DECREF(sys_path);
if (PyObject* modules = PyImport_GetModuleDict()) {
if (PyDict_GetItemString(modules, pkg_name.c_str())) {
if (PyDict_DelItemString(modules, pkg_name.c_str()) != 0) {
PyErr_Clear();
}
}
}
PyObject* module = PyImport_ImportModule(pkg_name.c_str());
if (!module) {
PyObject *ptype, *pvalue, *ptraceback;
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
error = "Failed to import module: " + format_python_error(ptype, pvalue, ptraceback);
Py_XDECREF(ptype);
Py_XDECREF(pvalue);
Py_XDECREF(ptraceback);
return nullptr;
}
return module;
}
PyObject* PythonInterpreter::load_module_from_whl(const std::string& file_path, const std::string& pkg_name, std::string& error)
{
if (!m_initialized) {
error = "Python interpreter not initialized";
return nullptr;
}
namespace fs = boost::filesystem;
fs::path whl_path(file_path);
if (!fs::exists(whl_path)) {
error = "File does not exist: " + file_path;
return nullptr;
}
fs::path extract_dir = whl_path.parent_path() / "__whl_extracted__" / pkg_name;
if (!fs::exists(extract_dir)) {
if (!extract_zip_to_directory(whl_path, extract_dir, error))
return nullptr;
}
return load_module_from_directory(extract_dir.string(), pkg_name, error);
}
bool PythonInterpreter::call_function(
PyObject* module, const std::string& function_name, const std::string& arg, std::string& result, std::string& error)
{
if (!m_initialized || !module) {
error = "Python interpreter not initialized or module is null";
return false;
}
PythonGILState gil;
PyObject* func = PyObject_GetAttrString(module, function_name.c_str());
if (!func || !PyCallable_Check(func)) {
Py_XDECREF(func);
error = "Function '" + function_name + "' not found or not callable";
return false;
}
PyObjectPtr args(PyTuple_New(1));
PyObjectPtr arg_str(PyUnicode_FromString(arg.c_str()));
PyTuple_SetItem(args.get(), 0, arg_str.release());
PyObjectPtr py_result(PyObject_CallObject(func, args.get()));
Py_DECREF(func);
if (!py_result) {
PyObject *ptype, *pvalue, *ptraceback;
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
error = "Function call failed: " + format_python_error(ptype, pvalue, ptraceback);
Py_XDECREF(ptype);
Py_XDECREF(pvalue);
Py_XDECREF(ptraceback);
return false;
}
result = py_object_to_string(py_result.get());
return true;
}
bool PythonInterpreter::call_function_no_args(PyObject* module, const std::string& function_name, std::string& result, std::string& error)
{
if (!m_initialized || !module) {
error = "Python interpreter not initialized or module is null";
return false;
}
PythonGILState gil;
PyObject* func = PyObject_GetAttrString(module, function_name.c_str());
if (!func || !PyCallable_Check(func)) {
Py_XDECREF(func);
error = "Function '" + function_name + "' not found or not callable";
return false;
}
PyObjectPtr py_result(PyObject_CallObject(func, nullptr));
Py_DECREF(func);
if (!py_result) {
PyObject *ptype, *pvalue, *ptraceback;
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
error = "Function call failed: " + format_python_error(ptype, pvalue, ptraceback);
Py_XDECREF(ptype);
Py_XDECREF(pvalue);
Py_XDECREF(ptraceback);
return false;
}
result = py_object_to_string(py_result.get());
return true;
}
std::string PythonInterpreter::py_object_to_string(PyObject* obj)
{
if (!obj) {
return "";
}
if (PyUnicode_Check(obj)) {
const char* str = PyUnicode_AsUTF8(obj);
return str ? std::string(str) : "";
}
PyObjectPtr str_obj(PyObject_Str(obj));
if (str_obj) {
const char* str = PyUnicode_AsUTF8(str_obj.get());
return str ? std::string(str) : "";
}
return "";
}
} // namespace Slic3r
+148
View File
@@ -0,0 +1,148 @@
#ifndef slic3r_PythonInterpreter_hpp_
#define slic3r_PythonInterpreter_hpp_
#include <Python.h>
#include <pytypedefs.h>
#include <string>
#include <memory>
#include <functional>
#include "libslic3r/libslic3r.h"
namespace pybind11 {
class scoped_interpreter;
class error_already_set;
}
namespace Slic3r {
// Print a Python exception's full traceback to sys.stderr (tee'd to the session
// log) WITHOUT consuming err.
//
// Used by the ORCA_PY_OVERRIDE_AUDITED trampoline macro: the
// traceback is logged centrally at the C++<->Python boundary, then the original
// error_already_set is rethrown intact so downstream C++ catchers can still build
// the user-facing dialog from err.what(). Because it is non-destructive it does
// NOT call restore()/PyErr_Print() (those will empty err); it prints via Python's
// traceback module instead.
void log_python_exception_keep(pybind11::error_already_set& err);
// RAII wrapper for Python interpreter initialization/finalization
class PythonInterpreter
{
public:
static PythonInterpreter& instance();
// Initialize the Python interpreter
bool initialize();
// Check if interpreter is initialized
bool is_initialized() const { return m_initialized; }
const std::string& last_error() const { return m_last_error; }
// Shared user-writable package directory added to sys.path for plugins.
static std::string shared_packages_dir();
// Bundled Python executable path, or empty when no executable is found.
static std::string bundled_python_executable();
// Bundled uv executable path, or empty when uv is not bundled/found.
static std::string bundled_uv_path();
// Python ABI tag for the bundled interpreter, e.g. "cp312".
static std::string python_abi_tag();
// Finalize the Python interpreter.
void shutdown();
// Add a filesystem path to sys.path if not already present.
bool add_sys_path(const std::string& path, std::string& error);
// Execute a Python string and return result
bool execute_string(const std::string& code, std::string& error);
// Load a Python module from file path
PyObject* load_module_from_file(const std::string& file_path, std::string& error);
PyObject* load_module_from_whl(const std::string& whl_path, const std::string& pkg_name, std::string& error);
PyObject* load_module_from_directory(const std::string& dir_path, const std::string& pkg_name, std::string& error);
// Call a Python function with string argument, return string result
bool call_function(PyObject* module, const std::string& function_name,
const std::string& arg, std::string& result, std::string& error);
// Call a Python function with no arguments, return string result
bool call_function_no_args(PyObject* module, const std::string& function_name,
std::string& result, std::string& error);
// Helper to get string from Python object
static std::string py_object_to_string(PyObject* obj);
// Destructor finalizes Python if shutdown() was not called explicitly.
~PythonInterpreter();
private:
PythonInterpreter() = default;
PythonInterpreter(const PythonInterpreter&) = delete;
PythonInterpreter& operator=(const PythonInterpreter&) = delete;
bool m_initialized = false;
PyThreadState* m_main_thread_state = nullptr; // thread state saved after releasing GIL post-initialize
std::unique_ptr<pybind11::scoped_interpreter> m_interpreter;
std::string m_last_error;
};
// RAII helper for Python GIL (Global Interpreter Lock)
class PythonGILState
{
public:
PythonGILState() {
m_state = PyGILState_Ensure();
}
~PythonGILState() {
PyGILState_Release(m_state);
}
private:
PyGILState_STATE m_state;
};
// RAII helper for Python object references
class PyObjectPtr
{
public:
explicit PyObjectPtr(PyObject* obj = nullptr) : m_obj(obj) {}
~PyObjectPtr() {
if (m_obj) {
Py_DECREF(m_obj);
}
}
PyObject* get() const { return m_obj; }
PyObject* release() {
PyObject* temp = m_obj;
m_obj = nullptr;
return temp;
}
void reset(PyObject* obj = nullptr) {
if (m_obj) {
Py_DECREF(m_obj);
}
m_obj = obj;
}
operator bool() const { return m_obj != nullptr; }
private:
PyObject* m_obj;
PyObjectPtr(const PyObjectPtr&) = delete;
PyObjectPtr& operator=(const PyObjectPtr&) = delete;
};
} // namespace Slic3r
#endif /* slic3r_PythonInterpreter_hpp_ */
+398
View File
@@ -0,0 +1,398 @@
#include "PythonPluginBridge.hpp"
#include <boost/log/trivial.hpp>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <pybind11/embed.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "PythonInterpreter.hpp"
#include "PluginHostApi.hpp"
#include "PyPluginPackage.hpp"
#include "PyPluginTrampoline.hpp"
#include "pluginTypes/gcode/GCodePluginCapability.hpp"
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
#include "pluginTypes/script/ScriptPluginCapability.hpp"
namespace py = pybind11;
namespace Slic3r {
namespace {
// Python plugin discovery is a two-step capture:
// 1) PluginLoader sets an active plugin key and imports the Python module.
// 2) Python decorators/API calls enter these pybind callbacks without receiving the
// C++ PluginDescriptor, so the callbacks use the active key to attach Python classes
// to the plugin currently being loaded.
//
// The pending maps hold Python class objects, not plugin instances. Instances are created
// only after the package class has had a chance to register every capability.
thread_local std::string g_active_plugin_key;
std::mutex g_registry_mutex;
std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities;
std::unordered_map<std::string, py::object> g_pending_package;
struct PluginInstanceHandle
{
// The C++ plugin interface points into a Python object. Keep both alive through one
// shared control block; CapturedCapability later exposes an aliasing shared_ptr to plugin.
std::shared_ptr<PluginCapabilityInterface> plugin;
py::object keep_alive;
~PluginInstanceHandle()
{
if (keep_alive) {
if (Py_IsInitialized()) {
// Dropping a py::object decrefs the Python object, so reacquire the GIL.
PythonGILState gil;
keep_alive = py::object();
} else {
// During interpreter shutdown it is no longer safe to decref Python objects.
// release() forgets the wrapper ownership without touching Python runtime state.
(void) keep_alive.release();
}
}
}
};
} // namespace
PythonPluginBridge& PythonPluginBridge::instance()
{
static PythonPluginBridge bridge;
return bridge;
}
void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
{
PythonGILState gil;
BOOST_LOG_TRIVIAL(info) << "Beginning Python plugin capture for key " << plugin_key;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
// Start from a clean slot in case a previous failed load left pending Python classes
// for this same entry path.
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
}
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by
// Python code on this thread are attributed to this plugin.
g_active_plugin_key = plugin_key;
}
std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& plugin_key, std::string& error)
{
PythonGILState gil;
BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << plugin_key;
// Phase 1: run the package class's register_capabilities() while the active key is
// still set. That method is expected to call orca.register_capability() once per
// capability class, and register_capability() needs g_active_plugin_key to know which
// pending bucket to append to.
{
auto clear_active_key = [&plugin_key]() {
if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear();
};
auto discard_pending_for_key = [&plugin_key]() {
std::lock_guard<std::mutex> lock(g_registry_mutex);
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
};
try {
// The @orca.plugin decorator records the package class during module import.
// Move it into a local py::object and remove it from the pending map so the
// registry no longer owns it once finalization starts.
py::object package_cls;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_package.find(plugin_key);
if (it != g_pending_package.end()) {
package_cls = it->second;
g_pending_package.erase(it);
}
}
if (!package_cls) {
error = "Plugin did not register a package class; decorate it with @orca.plugin";
BOOST_LOG_TRIVIAL(error) << error << " for key " << plugin_key;
discard_pending_for_key();
clear_active_key();
return {};
}
// The package instance is only a registration coordinator. It is not returned
// to the rest of the plugin system; only the capability classes it registers
// are kept.
py::object package = package_cls();
package.attr("register_capabilities")();
} catch (py::error_already_set& err) {
log_python_exception_keep(err);
error = err.what();
BOOST_LOG_TRIVIAL(error) << "Plugin register_capabilities raised Python exception for key " << plugin_key
<< " error=" << error;
discard_pending_for_key();
clear_active_key();
return {};
} catch (const std::exception& ex) {
error = ex.what();
BOOST_LOG_TRIVIAL(error) << "Plugin register_capabilities raised exception for key " << plugin_key
<< " error=" << error;
discard_pending_for_key();
clear_active_key();
return {};
}
}
// Phase 2: move the capability classes that register_capabilities() appended into a
// local vector. From this point the pending registry no longer owns these py::objects.
std::vector<py::object> classes;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_capabilities.find(plugin_key);
if (it != g_pending_capabilities.end()) {
classes = std::move(it->second);
g_pending_capabilities.erase(it);
}
}
// Registration is complete. Later register_capability() calls should fail instead of
// accidentally attaching themselves to this plugin.
if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear();
BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << plugin_key;
std::vector<CapturedCapability> capabilities;
capabilities.reserve(classes.size());
// Phase 3: instantiate each registered capability class and convert it to the common
// C++ interface used by the rest of OrcaSlicer.
for (auto& cls : classes) {
try {
py::object instance = cls();
if (!py::isinstance<PluginCapabilityInterface>(instance)) {
error = "Registered capability must inherit from a PluginCapability base";
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << plugin_key
<< " error=" << error;
return {};
}
auto capability_iface = instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
if (!capability_iface) {
error = "Failed to cast Python capability to PluginCapabilityInterface";
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << plugin_key
<< " error=" << error;
return {};
}
// This is a registered capability, not the transient orca.base package.
// get_name() is required on capabilities and is cached for preset lookup.
std::string name = capability_iface->get_name();
// Capability names feed ';'-delimited config/preset serialization and drive
// dispatch, so unlike display names they cannot be silently rewritten — a ';'
// here is a hard error that rejects the whole plugin capture.
if (name.find(';') != std::string::npos) {
error = "Capability name must not contain ';': " + name;
BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << plugin_key
<< " error=" << error;
return {};
}
auto handle = std::make_shared<PluginInstanceHandle>();
handle->keep_alive = instance;
handle->plugin = std::move(capability_iface);
CapturedCapability captured;
// Return a shared_ptr<PluginCapabilityInterface> while keeping PluginInstanceHandle
// as the owner, so the Python instance stays alive as long as the C++ interface does.
captured.instance = std::shared_ptr<PluginCapabilityInterface>(handle, handle->plugin.get());
captured.name = std::move(name);
capabilities.emplace_back(std::move(captured));
} catch (py::error_already_set& err) {
// Direct Python call (cls() / get_name() above), not a trampoline override —
// log the traceback here. GIL is held for the duration of finalize_plugin_capture.
log_python_exception_keep(err);
error = err.what();
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << plugin_key
<< " error=" << error;
return {};
} catch (const std::exception& ex) {
error = ex.what();
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << plugin_key
<< " error=" << error;
return {};
}
}
BOOST_LOG_TRIVIAL(info) << "Instantiated " << capabilities.size() << " Python capability instance(s) for key " << plugin_key;
return capabilities;
}
void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
{
PythonGILState gil;
BOOST_LOG_TRIVIAL(warning) << "Cancelling Python plugin capture for key " << plugin_key;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
// Import or dependency setup failed before finalization. Drop anything the module
// may already have registered under this key.
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
}
if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear();
}
void PythonPluginBridge::clear_pending_captures()
{
if (!Py_IsInitialized()) {
std::lock_guard<std::mutex> lock(g_registry_mutex);
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size()
<< " pending Python plugin capture(s) without Python interpreter";
// py::object destruction would decref Python objects. If the interpreter is already
// gone, release the wrappers instead and intentionally skip decref.
for (auto& [plugin_key, plugins] : g_pending_capabilities) {
(void) plugin_key;
for (py::object& plugin : plugins)
(void) plugin.release();
}
g_pending_capabilities.clear();
for (auto& [plugin_key, pkg] : g_pending_package) {
(void) plugin_key;
(void) pkg.release();
}
g_pending_package.clear();
g_active_plugin_key.clear();
return;
}
// Normal shutdown path: hold the GIL and let py::object destructors decref cleanly.
PythonGILState gil;
std::lock_guard<std::mutex> lock(g_registry_mutex);
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
g_pending_capabilities.clear();
g_pending_package.clear();
g_active_plugin_key.clear();
}
void bind_python_api(pybind11::module_& m)
{
m.doc() = "OrcaSlicer plugin API";
auto pluginTypes = py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
.value("PostProcessing", PluginCapabilityType::PostProcessing)
.value("PrinterConnection", PluginCapabilityType::PrinterConnection)
.value("Automation", PluginCapabilityType::Automation)
.value("Analysis", PluginCapabilityType::Analysis)
.value("Importer", PluginCapabilityType::Importer)
.value("Exporter", PluginCapabilityType::Exporter)
.value("Visualization", PluginCapabilityType::Visualization)
.value("Script", PluginCapabilityType::Script)
.value("Unknown", PluginCapabilityType::Unknown)
.export_values();
py::enum_<PluginResult>(m, "PluginResult", "Execution summary code")
.value("Success", PluginResult::Success)
.value("Skipped", PluginResult::Skipped)
.value("RecoverableError", PluginResult::RecoverableError)
.value("FatalError", PluginResult::FatalError)
.export_values();
py::class_<PluginContext>(m, "PluginContext", "Context shared with plugin entry points")
.def(py::init<>())
.def_readwrite("orca_version", &PluginContext::orca_version);
py::class_<ExecutionResult>(m, "ExecutionResult", "Structured execution outcome")
.def(py::init<>())
.def(py::init<PluginResult, std::string, std::string>())
.def_readwrite("status", &ExecutionResult::status)
.def_readwrite("message", &ExecutionResult::message)
.def_readwrite("data", &ExecutionResult::data)
.def_static("success", &ExecutionResult::success, py::arg("message") = std::string(), py::arg("data") = std::string())
.def_static("skipped", &ExecutionResult::skipped, py::arg("message") = std::string())
.def_static("failure", &ExecutionResult::failure, py::arg("status"), py::arg("message"), py::arg("data") = std::string());
py::class_<PluginCapabilityInterface, PyPluginInterfaceTrampoline, std::shared_ptr<PluginCapabilityInterface>>(m, "PythonPluginBase")
.def(py::init<>())
.def("get_name", &PluginCapabilityInterface::get_name)
.def("get_type", &PluginCapabilityInterface::get_type)
.def("on_load", &PluginCapabilityInterface::on_load)
.def("on_unload", &PluginCapabilityInterface::on_unload);
// Expose the package marker base as orca.base. @orca.plugin later verifies that the
// decorated class derives from this exact pybind-registered C++ type.
py::class_<PyPluginPackage, PyPluginPackageTrampoline>(m, "base")
.def(py::init<>())
.def("register_capabilities", &PyPluginPackage::register_capabilities);
BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings";
// Make sure you register your bindings here
GCodePluginCapability::RegisterBindings(m, pluginTypes);
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes);
ScriptPluginCapability::RegisterBindings(m, pluginTypes);
PluginHostApi::RegisterBindings(m);
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
m.def(
"register_capability",
[](py::object plugin_cls) {
if (g_active_plugin_key.empty()) {
throw py::value_error("register_capability() called outside plugin discovery context");
}
// Store capability classes only, not instances. Finalization instantiates them
// after the package has registered the full set for this plugin.
py::handle base = py::type::of<PluginCapabilityInterface>();
const int is_subclass = PyObject_IsSubclass(plugin_cls.ptr(), base.ptr());
if (is_subclass != 1) {
if (is_subclass < 0)
PyErr_Clear();
throw py::value_error("Registered class must inherit from a PluginCapability base");
}
std::lock_guard<std::mutex> lock(g_registry_mutex);
g_pending_capabilities[g_active_plugin_key].push_back(std::move(plugin_cls));
BOOST_LOG_TRIVIAL(debug) << "Registered Python plugin capability class for key " << g_active_plugin_key;
},
R"pbdoc(Register a PluginCapability subclass while OrcaSlicer loads your module.)pbdoc");
m.def("plugin", [](py::object cls) {
if (g_active_plugin_key.empty())
throw py::value_error("@orca.plugin used outside plugin discovery context");
if (!PyType_Check(cls.ptr()))
throw py::value_error("@orca.plugin must decorate a class");
// The decorator is only a marker/capture hook. It records the package class now;
// finalize_plugin_capture() instantiates it later and calls register_capabilities().
py::handle base = py::type::of<PyPluginPackage>();
const int is_subclass = PyObject_IsSubclass(cls.ptr(), base.ptr());
if (is_subclass != 1) {
if (is_subclass < 0)
PyErr_Clear();
throw py::value_error("@orca.plugin must decorate a subclass of orca.base");
}
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto& slot = g_pending_package[g_active_plugin_key];
if (slot)
throw py::value_error("multiple @orca.plugin classes registered; exactly one is allowed per plugin");
slot = cls;
}
return cls; // decorator returns the class unchanged
}, R"pbdoc(Mark the single plugin package class (the orca.base subclass) for this file.)pbdoc");
}
} // namespace Slic3r
#ifdef ORCA_PYTHON_STUBGEN_MODULE
PYBIND11_MODULE(orca, m) { Slic3r::bind_python_api(m); }
#else
PYBIND11_EMBEDDED_MODULE(orca, m) { Slic3r::bind_python_api(m); }
#endif
+50
View File
@@ -0,0 +1,50 @@
#ifndef slic3r_PythonPluginBridge_hpp_
#define slic3r_PythonPluginBridge_hpp_
#include <memory>
#include <string>
#include <vector>
#include "PythonPluginInterface.hpp"
namespace Slic3r {
// One materialized capability returned from finalize_plugin_capture: the C++ instance
// and its resolved get_name() (cached while the GIL was held).
struct CapturedCapability
{
std::shared_ptr<PluginCapabilityInterface> instance;
std::string name;
};
class PythonPluginBridge
{
public:
static PythonPluginBridge& instance();
// Mark the beginning of a plugin registration capture for the provided key (usually file path).
void begin_plugin_capture(const std::string& plugin_key);
// Finalize capture: the plugin class was recorded by the @orca.plugin decorator during
// import; run register_capabilities() (which registers each capability class), then
// instantiate every registered capability and cache its get_name().
// Returns one CapturedCapability per capability, or an empty vector on failure
// (error message populated).
std::vector<CapturedCapability> finalize_plugin_capture(
const std::string& plugin_key, std::string& error);
// Clear any pending registrations for the key. Safe to call when import fails.
void cancel_plugin_capture(const std::string& plugin_key);
// Clear all pending registrations before interpreter shutdown.
void clear_pending_captures();
private:
PythonPluginBridge() = default;
PythonPluginBridge(const PythonPluginBridge&) = delete;
PythonPluginBridge& operator=(const PythonPluginBridge&) = delete;
};
} // namespace Slic3r
#endif /* slic3r_PythonPluginBridge_hpp_ */
+119
View File
@@ -0,0 +1,119 @@
#ifndef slic3r_PythonPluginInterface_hpp_
#define slic3r_PythonPluginInterface_hpp_
#include <cctype>
#include <string>
#include <string_view>
#include <utility>
#include <pybind11/embed.h>
namespace Slic3r {
enum class PluginCapabilityType { PostProcessing = 0, PrinterConnection, Automation, Analysis, Importer, Exporter, Visualization, Script, Unknown };
inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
{
switch (type) {
case PluginCapabilityType::PostProcessing: return "post-processing";
case PluginCapabilityType::PrinterConnection: return "printer-connection";
case PluginCapabilityType::Automation: return "automation";
case PluginCapabilityType::Analysis: return "analysis";
case PluginCapabilityType::Importer: return "importer";
case PluginCapabilityType::Exporter: return "exporter";
case PluginCapabilityType::Visualization: return "visualization";
case PluginCapabilityType::Script: return "script";
default: return "unknown";
}
}
inline std::string plugin_capability_type_display_name(PluginCapabilityType type)
{
switch (type) {
case PluginCapabilityType::PostProcessing: return "Post-processing";
case PluginCapabilityType::PrinterConnection: return "Printer connection";
case PluginCapabilityType::Automation: return "Automation";
case PluginCapabilityType::Analysis: return "Analysis";
case PluginCapabilityType::Importer: return "Importer";
case PluginCapabilityType::Exporter: return "Exporter";
case PluginCapabilityType::Visualization: return "Visualization";
case PluginCapabilityType::Script: return "Script";
default: return "Unknown";
}
}
inline PluginCapabilityType plugin_capability_type_from_string(std::string_view value)
{
auto to_lower = [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); };
std::string lowered;
lowered.reserve(value.size());
for (unsigned char ch : value) {
lowered.push_back(to_lower(ch));
}
if (lowered == "post-processing")
return PluginCapabilityType::PostProcessing;
if (lowered == "printer-connection")
return PluginCapabilityType::PrinterConnection;
if (lowered == "automation")
return PluginCapabilityType::Automation;
if (lowered == "analysis")
return PluginCapabilityType::Analysis;
if (lowered == "importer")
return PluginCapabilityType::Importer;
if (lowered == "exporter")
return PluginCapabilityType::Exporter;
if (lowered == "visualization")
return PluginCapabilityType::Visualization;
if (lowered == "script")
return PluginCapabilityType::Script;
return PluginCapabilityType::Unknown;
}
struct PluginContext
{ std::string orca_version; };
enum class PluginResult { Success, Skipped, RecoverableError, FatalError };
struct ExecutionResult
{
PluginResult status = PluginResult::Success;
std::string message;
std::string data;
static ExecutionResult success(std::string message = {}, std::string data = {})
{ return {PluginResult::Success, std::move(message), std::move(data)}; }
static ExecutionResult skipped(std::string message = {})
{
return {PluginResult::Skipped, std::move(message), {}};
}
static ExecutionResult failure(PluginResult status, std::string message, std::string data = {})
{ return {status, std::move(message), std::move(data)}; }
};
class PluginCapabilityInterface
{
public:
virtual ~PluginCapabilityInterface() = default;
virtual std::string get_name() const = 0; // required — overridden in Python
virtual PluginCapabilityType get_type() const { return PluginCapabilityType::Unknown; } // optional — typed bases override
virtual void on_load() {}
virtual void on_unload() {}
// C++-only audit identity (never exposed to Python). Set by PluginLoader after
// plugin capture so trampoline calls can scope filesystem enforcement to this
// plugin. This is PluginDescriptor::plugin_key, the canonical runtime id.
void set_audit_plugin_key(std::string key) { m_audit_plugin_key = std::move(key); }
const std::string& audit_plugin_key() const { return m_audit_plugin_key; }
private:
std::string m_audit_plugin_key;
};
} // namespace Slic3r
#endif /* slic3r_PythonPluginInterface_hpp_ */
@@ -0,0 +1,30 @@
#include "GCodePluginCapability.hpp"
#include "GCodePluginCapabilityTrampoline.hpp"
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
namespace Slic3r {
void GCodePluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
{
(void) pluginTypes;
auto gcode = module.def_submodule("gcode", "G-code API");
py::class_<GCodePluginContext, PluginContext>(gcode, "GCodePluginContext", "Context shared with G-code plugins")
.def(py::init<>())
.def_readwrite("gcode_path", &GCodePluginContext::gcode_path)
.def_readwrite("host", &GCodePluginContext::host)
.def_readwrite("output_name", &GCodePluginContext::output_name);
py::class_<GCodePluginCapability, PluginCapabilityInterface, PyGCodePluginCapabilityTrampoline, std::shared_ptr<GCodePluginCapability>>(gcode, "GCodePluginCapabilityBase")
.def(py::init<>())
.def("get_type", &GCodePluginCapability::get_type)
.def("execute", &GCodePluginCapability::execute);
}
} // namespace Slic3r
@@ -0,0 +1,27 @@
#ifndef slic3r_GCodePluginCapability_hpp_
#define slic3r_GCodePluginCapability_hpp_
#include "../../PythonPluginInterface.hpp"
namespace Slic3r {
struct GCodePluginContext : public PluginContext {
std::string gcode_path;
std::string host;
std::string output_name;
};
class GCodePluginCapability : public PluginCapabilityInterface
{
public:
PluginCapabilityType get_type() const override { return PluginCapabilityType::PostProcessing; }
virtual ExecutionResult execute(const GCodePluginContext& ctx) = 0;
static void RegisterBindings(pybind11::module_ &module,
pybind11::enum_<PluginCapabilityType> &pluginTypes);
};
} // namespace Slic3r
#endif /* slic3r_GCodePluginCapability_hpp_ */
@@ -0,0 +1,35 @@
#ifndef slic3r_GCodePluginCapabilityTrampoline_hpp_
#define slic3r_GCodePluginCapabilityTrampoline_hpp_
#include <filesystem>
#include "../../PyPluginTrampoline.hpp"
#include "../../PluginAuditManager.hpp"
#include "GCodePluginCapability.hpp"
namespace Slic3r {
class PyGCodePluginCapabilityTrampoline : public PyPluginCommonTrampoline<GCodePluginCapability>
{
public:
using PyPluginCommonTrampoline<GCodePluginCapability>::PyPluginCommonTrampoline;
ExecutionResult execute(const GCodePluginContext& ctx) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[&] {
// G-code post-processing plugins may also write into the folder holding the
// current temp G-code file, in addition to the globally-allowed data_dir().
// The setup callback runs AFTER the context is constructed so the scoped root
// is not cleared by ScopedPluginAuditContext's constructor.
if (!ctx.gcode_path.empty())
::Slic3r::PluginAuditManager::instance().add_scoped_allowed_root(
std::filesystem::path(ctx.gcode_path).parent_path());
},
PYBIND11_OVERRIDE_PURE, ExecutionResult, GCodePluginCapability, execute, ctx);
}
};
} // namespace Slic3r
#endif
@@ -0,0 +1,121 @@
#include "PrinterAgentPluginCapability.hpp"
#include "PrinterAgentPluginCapabilityTrampoline.hpp"
#include "IPrinterAgent.hpp"
#include <pybind11/functional.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <utility>
namespace py = pybind11;
namespace Slic3r {
void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
{
(void) pluginTypes;
auto printer_agent_module = module.def_submodule("printer_agent", "Printer Agent API");
py::enum_<FilamentSyncMode>(printer_agent_module, "FilamentSyncMode")
.value("None_", FilamentSyncMode::none)
.value("Subscription", FilamentSyncMode::subscription)
.value("Pull", FilamentSyncMode::pull)
.export_values();
py::class_<AgentInfo>(printer_agent_module, "AgentInfo")
.def(py::init<>())
.def(py::init([](std::string id, std::string name, std::string version, std::string description) {
return AgentInfo{std::move(id), std::move(name), std::move(version), std::move(description)};
}),
py::arg("id"), py::arg("name"), py::arg("version"), py::arg("description"))
.def_readwrite("id", &AgentInfo::id)
.def_readwrite("name", &AgentInfo::name)
.def_readwrite("version", &AgentInfo::version)
.def_readwrite("description", &AgentInfo::description);
py::class_<detectResult>(printer_agent_module, "DetectResult")
.def(py::init<>())
.def_readwrite("result_msg", &detectResult::result_msg)
.def_readwrite("command", &detectResult::command)
.def_readwrite("dev_id", &detectResult::dev_id)
.def_readwrite("model_id", &detectResult::model_id)
.def_readwrite("dev_name", &detectResult::dev_name)
.def_readwrite("version", &detectResult::version)
.def_readwrite("bind_state", &detectResult::bind_state)
.def_readwrite("connect_type", &detectResult::connect_type);
py::class_<PrintParams>(printer_agent_module, "PrintParams")
.def(py::init<>())
.def_readwrite("dev_id", &PrintParams::dev_id)
.def_readwrite("task_name", &PrintParams::task_name)
.def_readwrite("project_name", &PrintParams::project_name)
.def_readwrite("preset_name", &PrintParams::preset_name)
.def_readwrite("filename", &PrintParams::filename)
.def_readwrite("config_filename", &PrintParams::config_filename)
.def_readwrite("plate_index", &PrintParams::plate_index)
.def_readwrite("ftp_folder", &PrintParams::ftp_folder)
.def_readwrite("ftp_file", &PrintParams::ftp_file)
.def_readwrite("ftp_file_md5", &PrintParams::ftp_file_md5)
.def_readwrite("nozzle_mapping", &PrintParams::nozzle_mapping)
.def_readwrite("ams_mapping", &PrintParams::ams_mapping)
.def_readwrite("ams_mapping2", &PrintParams::ams_mapping2)
.def_readwrite("ams_mapping_info", &PrintParams::ams_mapping_info)
.def_readwrite("nozzles_info", &PrintParams::nozzles_info)
.def_readwrite("connection_type", &PrintParams::connection_type)
.def_readwrite("comments", &PrintParams::comments)
.def_readwrite("origin_profile_id", &PrintParams::origin_profile_id)
.def_readwrite("stl_design_id", &PrintParams::stl_design_id)
.def_readwrite("origin_model_id", &PrintParams::origin_model_id)
.def_readwrite("print_type", &PrintParams::print_type)
.def_readwrite("dst_file", &PrintParams::dst_file)
.def_readwrite("dev_name", &PrintParams::dev_name)
.def_readwrite("dev_ip", &PrintParams::dev_ip)
.def_readwrite("use_ssl_for_ftp", &PrintParams::use_ssl_for_ftp)
.def_readwrite("use_ssl_for_mqtt", &PrintParams::use_ssl_for_mqtt)
.def_readwrite("username", &PrintParams::username)
.def_readwrite("password", &PrintParams::password)
.def_readwrite("task_bed_leveling", &PrintParams::task_bed_leveling)
.def_readwrite("task_flow_cali", &PrintParams::task_flow_cali)
.def_readwrite("task_vibration_cali", &PrintParams::task_vibration_cali)
.def_readwrite("task_layer_inspect", &PrintParams::task_layer_inspect)
.def_readwrite("task_record_timelapse", &PrintParams::task_record_timelapse)
.def_readwrite("task_use_ams", &PrintParams::task_use_ams)
.def_readwrite("task_bed_type", &PrintParams::task_bed_type)
.def_readwrite("extra_options", &PrintParams::extra_options)
.def_readwrite("auto_bed_leveling", &PrintParams::auto_bed_leveling)
.def_readwrite("auto_flow_cali", &PrintParams::auto_flow_cali)
.def_readwrite("auto_offset_cali", &PrintParams::auto_offset_cali)
.def_readwrite("task_ext_change_assist", &PrintParams::task_ext_change_assist)
.def_readwrite("try_emmc_print", &PrintParams::try_emmc_print);
py::class_<PrinterAgentPluginCapability, PluginCapabilityInterface, PyPrinterAgentPluginCapabilityTrampoline, std::shared_ptr<PrinterAgentPluginCapability>>(
printer_agent_module, "PrinterAgentBase")
.def(py::init<>())
.def("get_type", &PrinterAgentPluginCapability::get_type)
.def("get_agent_info", &PrinterAgentPluginCapability::get_agent_info)
.def("connect_printer", &PrinterAgentPluginCapability::connect_printer)
.def("disconnect_printer", &PrinterAgentPluginCapability::disconnect_printer)
.def("send_message", &PrinterAgentPluginCapability::send_message)
.def("send_message_to_printer", &PrinterAgentPluginCapability::send_message_to_printer)
.def("start_discovery", &PrinterAgentPluginCapability::start_discovery)
.def("bind_detect", &PrinterAgentPluginCapability::bind_detect)
.def("get_user_selected_machine", &PrinterAgentPluginCapability::get_user_selected_machine)
.def("set_user_selected_machine", &PrinterAgentPluginCapability::set_user_selected_machine)
.def("start_send_gcode_to_sdcard", &PrinterAgentPluginCapability::start_send_gcode_to_sdcard)
.def("start_local_print", &PrinterAgentPluginCapability::start_local_print)
.def("get_filament_sync_mode", &PrinterAgentPluginCapability::get_filament_sync_mode)
.def("fetch_filament_info", &PrinterAgentPluginCapability::fetch_filament_info)
.def("check_cert", &PrinterAgentPluginCapability::check_cert)
.def("install_device_cert", &PrinterAgentPluginCapability::install_device_cert)
.def("ping_bind", &PrinterAgentPluginCapability::ping_bind)
.def("bind", &PrinterAgentPluginCapability::bind)
.def("unbind", &PrinterAgentPluginCapability::unbind)
.def("start_print", &PrinterAgentPluginCapability::start_print)
.def("start_local_print_with_record", &PrinterAgentPluginCapability::start_local_print_with_record)
.def("start_sdcard_print", &PrinterAgentPluginCapability::start_sdcard_print);
}
} // namespace Slic3r
@@ -0,0 +1,83 @@
#ifndef slic3r_PrinterAgentPluginCapability_hpp_
#define slic3r_PrinterAgentPluginCapability_hpp_
#include "../../PythonPluginInterface.hpp"
#include "IPrinterAgent.hpp"
#include <functional>
#include <memory>
#include <string>
namespace Slic3r {
// A printer-agent plugin capability implements IPrinterAgent directly: the host
// drives it through the native IPrinterAgent surface and the Python plugin
// overrides the individual operations. The capability is registered with the
// NetworkAgentFactory and handed out as the live IPrinterAgent for the selected
// printer agent.
class PrinterAgentPluginCapability : public PluginCapabilityInterface, public IPrinterAgent
{
public:
static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes);
PluginCapabilityType get_type() const override { return PluginCapabilityType::PrinterConnection; }
// set_cloud_agent is the host-managed dependency injection point — the host hands the
// capability its ICloudServiceAgent — so it is the one operation kept native here. Every
// other IPrinterAgent operation is pure: the Python plugin must implement all of them.
void set_cloud_agent(std::shared_ptr<ICloudServiceAgent> cloud) final override { (void) cloud; }
AgentInfo get_agent_info() override = 0;
int connect_printer(
std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override = 0;
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override = 0;
bool start_discovery(bool start, bool sending) override = 0;
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override = 0;
std::string get_user_selected_machine() override = 0;
int set_user_selected_machine(std::string dev_id) override = 0;
int start_send_gcode_to_sdcard(PrintParams params,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn,
OnWaitFn wait_fn) override = 0;
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
FilamentSyncMode get_filament_sync_mode() const override = 0;
bool fetch_filament_info(std::string dev_id) override = 0;
int check_cert() override = 0;
void install_device_cert(std::string dev_id, bool lan_only) override = 0;
int ping_bind(std::string ping_code) override = 0;
int bind(std::string dev_ip,
std::string dev_id,
std::string sec_link,
std::string timezone,
bool improved,
OnUpdateStatusFn update_fn) override = 0;
int unbind(std::string dev_id) override = 0;
// request_bind_ticket has a std::string* out-param that cannot round-trip through a
// pybind11 override directly; the trampoline wraps it (the Python plugin returns a
// (result, ticket) tuple), so it stays pure here like the rest.
int request_bind_ticket(std::string* ticket) override = 0;
int set_server_callback(OnServerErrFn fn) override = 0;
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override = 0;
int start_local_print_with_record(PrintParams params,
OnUpdateStatusFn update_fn,
WasCancelledFn cancel_fn,
OnWaitFn wait_fn) override = 0;
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0;
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override = 0;
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override = 0;
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override = 0;
int set_on_message_fn(OnMessageFn fn) override = 0;
int set_on_user_message_fn(OnMessageFn fn) override = 0;
int set_on_local_connect_fn(OnLocalConnectedFn fn) override = 0;
int set_on_local_message_fn(OnMessageFn fn) override = 0;
int set_queue_on_main_fn(QueueOnMainFn fn) override = 0;
};
} // namespace Slic3r
#endif /* slic3r_PrinterAgentPluginCapability_hpp_ */
@@ -0,0 +1,237 @@
#ifndef slic3r_PrinterAgentPluginCapabilityTrampoline_hpp_
#define slic3r_PrinterAgentPluginCapabilityTrampoline_hpp_
#include "PrinterAgentPluginCapability.hpp"
#include "../../PyPluginTrampoline.hpp"
#include "IPrinterAgent.hpp"
namespace Slic3r {
class PyPrinterAgentPluginCapabilityTrampoline : public PyPluginCommonTrampoline<PrinterAgentPluginCapability>
{
public:
using PyPluginCommonTrampoline<PrinterAgentPluginCapability>::PyPluginCommonTrampoline;
AgentInfo get_agent_info() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability,
get_agent_info);
}
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id,
dev_ip, username, password, use_ssl);
}
int disconnect_printer() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer);
}
int send_message(std::string dev_id, std::string json_str, int qos, int flag) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id,
json_str, qos, flag);
}
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer,
dev_id, json_str, qos, flag);
}
bool start_discovery(bool start, bool sending) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start,
sending);
}
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip,
sec_link, detect);
}
std::string get_user_selected_machine() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability,
get_user_selected_machine);
}
int set_user_selected_machine(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
set_user_selected_machine, dev_id);
}
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn);
}
int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print,
params, update_fn, cancel_fn);
}
FilamentSyncMode get_filament_sync_mode() const override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability,
get_filament_sync_mode);
}
bool fetch_filament_info(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id);
}
int check_cert() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert);
}
void install_device_cert(std::string dev_id, bool lan_only) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
lan_only);
}
int ping_bind(std::string ping_code) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code);
}
int bind(std::string dev_ip, std::string dev_id, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
sec_link, timezone, improved, update_fn);
}
int unbind(std::string dev_id) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id);
}
int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params,
update_fn, cancel_fn, wait_fn);
}
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_local_print_with_record, params, update_fn, cancel_fn, wait_fn);
}
int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params,
update_fn, cancel_fn);
}
int set_server_callback(OnServerErrFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn);
}
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn);
}
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn,
fn);
}
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn,
fn);
}
int set_on_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn);
}
int set_on_user_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn);
}
int set_on_local_connect_fn(OnLocalConnectedFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn);
}
int set_on_local_message_fn(OnMessageFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn);
}
int set_queue_on_main_fn(QueueOnMainFn fn) override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn);
}
// request_bind_ticket returns its ticket through a std::string* out-param, which pybind11
// cannot marshal back through a plain override. We dispatch manually: the Python plugin
// returns a (result, ticket) tuple, which we unpack into the int result and the out-param.
int request_bind_ticket(std::string* ticket) override
{
ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
pybind11::gil_scoped_acquire gil;
pybind11::function override =
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
if (!override)
pybind11::pybind11_fail("Tried to call pure virtual function \"PrinterAgentPluginCapability::request_bind_ticket\"");
try {
pybind11::tuple result = override().cast<pybind11::tuple>();
if (ticket)
*ticket = result[1].cast<std::string>();
return result[0].cast<int>();
} catch (pybind11::error_already_set& err) {
::Slic3r::log_python_exception_keep(err);
throw;
}
}
};
} // namespace Slic3r
#endif /* slic3r_PrinterAgentPluginCapabilityTrampoline_hpp_ */
@@ -0,0 +1,24 @@
#include "ScriptPluginCapability.hpp"
#include "ScriptPluginCapabilityTrampoline.hpp"
#include <boost/log/trivial.hpp>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
namespace Slic3r {
void ScriptPluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
{
(void) pluginTypes;
BOOST_LOG_TRIVIAL(debug) << "Registering orca.script bindings";
auto script = module.def_submodule("script", "Script Plugins API");
py::class_<ScriptPluginCapability, PluginCapabilityInterface, PyScriptPluginCapabilityTrampoline, std::shared_ptr<ScriptPluginCapability>>(script, "ScriptPluginCapabilityBase")
.def(py::init<>())
.def("get_type", &ScriptPluginCapability::get_type)
.def("execute", &ScriptPluginCapability::execute);
}
} // namespace Slic3r
@@ -0,0 +1,19 @@
#ifndef slic3r_ScriptPluginCapability_hpp_
#define slic3r_ScriptPluginCapability_hpp_
#include "../../PythonPluginInterface.hpp"
namespace Slic3r {
class ScriptPluginCapability : public PluginCapabilityInterface
{
public:
PluginCapabilityType get_type() const override { return PluginCapabilityType::Script; }
virtual ExecutionResult execute() = 0;
static void RegisterBindings(pybind11::module_ &module,
pybind11::enum_<PluginCapabilityType> &pluginTypes);
};
} // namespace Slic3r
#endif /* slic3r_ScriptPluginCapability_hpp_ */
@@ -0,0 +1,26 @@
#ifndef slic3r_ScriptPluginCapabilityTrampoline_hpp_
#define slic3r_ScriptPluginCapabilityTrampoline_hpp_
#include "ScriptPluginCapability.hpp"
#include "../../PyPluginTrampoline.hpp"
namespace Slic3r {
class PyScriptPluginCapabilityTrampoline : public PyPluginCommonTrampoline<ScriptPluginCapability>
{
public:
using PyPluginCommonTrampoline<ScriptPluginCapability>::PyPluginCommonTrampoline;
ExecutionResult execute() override
{
ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {},
PYBIND11_OVERRIDE_PURE,
ExecutionResult,
ScriptPluginCapability,
execute);
}
};
} // namespace Slic3r
#endif