mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-18 13:10:54 +00:00
feat: Python Plugins
This commit is contained in:
183 files changed
+49955
-2120
No files matched your search
@@ -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 ()
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(¬_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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+545
-552
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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,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@"
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
// ========================================================================
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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", ¤t_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", ¤t_plater, py::return_value_policy::reference);
|
||||
host.def("model", []() -> Model& {
|
||||
return current_plater()->model();
|
||||
}, py::return_value_policy::reference);
|
||||
host.def("preset_bundle", ¤t_preset_bundle, py::return_value_policy::reference);
|
||||
|
||||
// UI: native dialogs and interactive HTML windows for plugins.
|
||||
PluginHostUi::RegisterBindings(host);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PluginHostApi
|
||||
{
|
||||
public:
|
||||
static void RegisterBindings(pybind11::module_& module);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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
|
||||
@@ -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_ */
|
||||
@@ -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_ */
|
||||
+237
@@ -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
|
||||
Reference in new issue
Block a user