Compare commits

..

1 Commits

Author SHA1 Message Date
Ian Chua
277ff35325 feat: add option to use mold to build deps on linux 2026-08-31 14:50:24 +08:00
62 changed files with 360 additions and 1398 deletions

View File

@@ -151,7 +151,7 @@ jobs:
if [ -z "${{ vars.SELF_HOSTED }}" ]; then if [ -z "${{ vars.SELF_HOSTED }}" ]; then
brew install automake texinfo libtool pkgconf yasm nasm brew install automake texinfo libtool pkgconf yasm nasm
fi fi
./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15 ./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15
(cd "${{ github.workspace }}/deps/build/${{ inputs.arch }}" && \ (cd "${{ github.workspace }}/deps/build/${{ inputs.arch }}" && \
find . -mindepth 1 -maxdepth 1 ! -name 'OrcaSlicer_dep' -exec rm -rf {} +) find . -mindepth 1 -maxdepth 1 ! -name 'OrcaSlicer_dep' -exec rm -rf {} +)

View File

@@ -145,7 +145,7 @@ jobs:
env: env:
ORCA_TESTS_BUILD_ONLY: ${{ inputs.arch == 'arm64' && '1' || '' }} ORCA_TESTS_BUILD_ONLY: ${{ inputs.arch == 'arm64' && '1' || '' }}
run: | run: |
./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }} ./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }}
- name: Pack unit tests mac - name: Pack unit tests mac
if: runner.os == 'macOS' && !inputs.macos-combine-only && inputs.arch == 'arm64' if: runner.os == 'macOS' && !inputs.macos-combine-only && inputs.arch == 'arm64'
@@ -204,7 +204,7 @@ jobs:
if: runner.os == 'macOS' && inputs.macos-combine-only if: runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
run: | run: |
./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a universal -t 10.15 ./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a universal -t 10.15
# Thanks to RaySajuuk, it's working now # Thanks to RaySajuuk, it's working now
- name: Sign app and notary - name: Sign app and notary
@@ -385,6 +385,13 @@ jobs:
dir "C:/Program Files (x86)/Windows Kits/10/Include" dir "C:/Program Files (x86)/Windows Kits/10/Include"
choco install nsis choco install nsis
- name: Install pkg-config
# FFmpeg is discovered via pkg-config (pkg_check_modules LIBAV in
# src/slic3r/CMakeLists.txt); the Windows runners don't ship it.
if: runner.os == 'Windows' && !vars.SELF_HOSTED
run: |
choco install pkgconfiglite -y
- name: Build slicer Win - name: Build slicer Win
if: runner.os == 'Windows' if: runner.os == 'Windows'
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}

View File

@@ -476,7 +476,8 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory. # WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory.
# We pick it from environment if it is not defined in another way # We pick it from environment if it is not defined in another way
# ORCA: Removed Netfabb STL fixing service support in favor of CGAL. # ORCA: Removed Netfabb STL fixing service support in favor of CGAL.
# if(WIN32) if(WIN32)
find_package(PkgConfig REQUIRED)
# if(NOT DEFINED WIN10SDK_PATH) # if(NOT DEFINED WIN10SDK_PATH)
# if(DEFINED ENV{WIN10SDK_PATH}) # if(DEFINED ENV{WIN10SDK_PATH})
# set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}") # set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}")
@@ -512,7 +513,7 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# else() # else()
# message("Building without Win10 Netfabb STL fixing service support") # message("Building without Win10 Netfabb STL fixing service support")
# endif() # endif()
# endif() endif()
if (APPLE) if (APPLE)
message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}") message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}")

View File

@@ -8,7 +8,7 @@ SCRIPT_PATH=$(dirname "$(readlink -f "${0}")")
pushd "${SCRIPT_PATH}" > /dev/null pushd "${SCRIPT_PATH}" > /dev/null
function usage() { function usage() {
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L]" echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L][-M]"
echo " -1: limit builds to one core (where possible)" echo " -1: limit builds to one core (where possible)"
echo " -j N: limit builds to N cores (where possible)" echo " -j N: limit builds to N cores (where possible)"
echo " -b: build in Debug mode" echo " -b: build in Debug mode"
@@ -28,6 +28,7 @@ function usage() {
echo " -u: install system dependencies (asks for sudo password; build prerequisite)" echo " -u: install system dependencies (asks for sudo password; build prerequisite)"
echo " -l: use Clang instead of GCC (default: GCC)" echo " -l: use Clang instead of GCC (default: GCC)"
echo " -L: use ld.lld as linker (if available)" echo " -L: use ld.lld as linker (if available)"
echo " -M: use mold as linker (if available)"
echo "For a first use, you want to './${SCRIPT_NAME} -u'" echo "For a first use, you want to './${SCRIPT_NAME} -u'"
echo " and then './${SCRIPT_NAME} -dsi'" echo " and then './${SCRIPT_NAME} -dsi'"
echo "For a GitHub Actions-like Linux build locally, use './${SCRIPT_NAME} -g -istrlL'" echo "For a GitHub Actions-like Linux build locally, use './${SCRIPT_NAME} -g -istrlL'"
@@ -41,7 +42,7 @@ unset name
BUILD_DIR=build BUILD_DIR=build
BUILD_CONFIG=Release BUILD_CONFIG=Release
FORWARDED_ARGS=() FORWARDED_ARGS=()
while getopts ":1j:bcCdDeFghiprstulL" opt ; do while getopts ":1j:bcCdDeFghiprstulLM" opt ; do
case ${opt} in case ${opt} in
1 ) 1 )
export CMAKE_BUILD_PARALLEL_LEVEL=1 export CMAKE_BUILD_PARALLEL_LEVEL=1
@@ -118,6 +119,10 @@ while getopts ":1j:bcCdDeFghiprstulL" opt ; do
USE_LLD="1" USE_LLD="1"
FORWARDED_ARGS+=("-L") FORWARDED_ARGS+=("-L")
;; ;;
M )
USE_MOLD="1"
FORWARDED_ARGS+=("-M")
;;
* ) * )
echo "Unknown argument '${opt}', aborting." echo "Unknown argument '${opt}', aborting."
exit 1 exit 1
@@ -135,6 +140,11 @@ if [[ -n "${CLEAN_DOCKER_IMAGE}" ]] && [[ -z "${USE_DOCKER}" ]] ; then
exit 1 exit 1
fi fi
if [[ -n "${USE_LLD}" ]] && [[ -n "${USE_MOLD}" ]] ; then
echo "Error: -L and -M are mutually exclusive."
exit 1
fi
function check_available_memory_and_disk() { function check_available_memory_and_disk() {
FREE_MEM_GB=$(free --gibi --total | grep 'Mem' | rev | cut --delimiter=" " --fields=1 | rev) FREE_MEM_GB=$(free --gibi --total | grep 'Mem' | rev | cut --delimiter=" " --fields=1 | rev)
MIN_MEM_GB=10 MIN_MEM_GB=10
@@ -504,6 +514,18 @@ if [[ -n "${USE_LLD}" ]] ; then
fi fi
fi fi
# Configure use of mold as the linker when requested
export CMAKE_MOLD_LINKER_ARGS=()
if [[ -n "${USE_MOLD}" ]] ; then
if command -v mold >/dev/null 2>&1 ; then
MOLD_BIN=$(command -v mold)
export CMAKE_MOLD_LINKER_ARGS=(-DCMAKE_LINKER="${MOLD_BIN}" -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=mold -DCMAKE_SHARED_LINKER_FLAGS=-fuse-ld=mold -DCMAKE_MODULE_LINKER_FLAGS=-fuse-ld=mold)
else
echo "Error: mold not found. Please install the 'mold' package or omit -M."
exit 1
fi
fi
export CMAKE_CCACHE_ARGS=() export CMAKE_CCACHE_ARGS=()
CMAKE_CCACHE=${CMAKE_CCACHE:-} CMAKE_CCACHE=${CMAKE_CCACHE:-}
if [ -n "$CMAKE_CCACHE" ]; then if [ -n "$CMAKE_CCACHE" ]; then
@@ -536,7 +558,7 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}") BUILD_ARGS+=(-DCMAKE_BUILD_TYPE="${BUILD_CONFIG}")
fi fi
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}" print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_MOLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
print_and_run cmake --build deps/$BUILD_DIR -j1 print_and_run cmake --build deps/$BUILD_DIR -j1
fi fi
@@ -556,7 +578,7 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
BUILD_ARGS+=(-DORCA_UPDATER_SIG_KEY="${ORCA_UPDATER_SIG_KEY}") BUILD_ARGS+=(-DORCA_UPDATER_SIG_KEY="${ORCA_UPDATER_SIG_KEY}")
fi fi
print_and_run cmake -S . -B $BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G "Ninja Multi-Config" \ print_and_run cmake -S . -B $BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_MOLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G "Ninja Multi-Config" \
-DSLIC3R_PCH=${SLIC3R_PRECOMPILED_HEADERS} \ -DSLIC3R_PCH=${SLIC3R_PRECOMPILED_HEADERS} \
-DORCA_TOOLS=ON \ -DORCA_TOOLS=ON \
"${COLORED_OUTPUT}" \ "${COLORED_OUTPUT}" \

View File

@@ -4,7 +4,7 @@ set -e
set -o pipefail set -o pipefail
SECONDS=0 SECONDS=0
while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do while getopts ":dpa:snt:xbc:i:1Tuh" opt; do
case "${opt}" in case "${opt}" in
d ) d )
export BUILD_TARGET="deps" export BUILD_TARGET="deps"
@@ -38,8 +38,8 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
i ) i )
export CMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH:+$CMAKE_IGNORE_PREFIX_PATH;}$OPTARG" export CMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH:+$CMAKE_IGNORE_PREFIX_PATH;}$OPTARG"
;; ;;
j ) 1 )
export CMAKE_BUILD_PARALLEL_LEVEL="$OPTARG" export CMAKE_BUILD_PARALLEL_LEVEL=1
;; ;;
T ) T )
export BUILD_TESTS="1" export BUILD_TESTS="1"
@@ -58,7 +58,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
echo " -b: Build without reconfiguring CMake" echo " -b: Build without reconfiguring CMake"
echo " -c: Set CMake build configuration, default is Release" echo " -c: Set CMake build configuration, default is Release"
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew" echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)" echo " -1: Use single job for building"
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)" echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
exit 0 exit 0
;; ;;

View File

@@ -78,7 +78,7 @@ struct PlateBBoxData
int first_extruder = 0; int first_extruder = 0;
float nozzle_diameter = 0.4; float nozzle_diameter = 0.4;
std::string bed_type; std::string bed_type;
float first_layer_time = 0.0f; float first_layer_time;
// version 1: use view type ColorPrint (filament color) // version 1: use view type ColorPrint (filament color)
// version 2: use view type FilamentId (filament id) // version 2: use view type FilamentId (filament id)
int version = 2; int version = 2;

View File

@@ -165,6 +165,7 @@ public:
//BBS //BBS
unsigned int m_last_additional_fan_speed;
int m_last_bed_temperature; int m_last_bed_temperature;
bool m_last_bed_temperature_reached; bool m_last_bed_temperature_reached;
double m_lifted; double m_lifted;

View File

@@ -915,18 +915,6 @@ if (APPLE)
endif () endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
elseif (WIN32)
# Prebuilt shared FFmpeg from the deps install. Windows has no pkg-config,
# so resolve the import libraries out of the deps prefix directly; the DLLs
# are copied next to the executable by the top level CMakeLists.
find_library(LIBAVCODEC_LIBRARY NAMES avcodec PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES swscale PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES avutil PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
message(FATAL_ERROR "FFmpeg (avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.")
endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
else () else ()
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavcodec libavcodec

View File

@@ -55,6 +55,7 @@ private:
Label* m_text_label; Label* m_text_label;
wxStaticBitmap* m_icon_bitmap; wxStaticBitmap* m_icon_bitmap;
int m_target_size;
std::string m_icon_name; std::string m_icon_name;
ScalableBitmap m_icon; ScalableBitmap m_icon;
}; };

View File

@@ -60,6 +60,7 @@ class AboutDialog : public DPIDialog
wxHtmlWindow* m_html; wxHtmlWindow* m_html;
wxStaticBitmap* m_logo; wxStaticBitmap* m_logo;
int m_copy_rights_btn_id { wxID_ANY }; int m_copy_rights_btn_id { wxID_ANY };
int m_copy_version_btn_id { wxID_ANY };
public: public:
AboutDialog(); AboutDialog();

View File

@@ -93,6 +93,7 @@ private:
CenteredTitle* m_title_ctrl { nullptr }; CenteredTitle* m_title_ctrl { nullptr };
wxString m_titleText; wxString m_titleText;
wxAuiToolBarItem* m_account_item;
wxAuiToolBarItem* m_model_store_item; wxAuiToolBarItem* m_model_store_item;
//wxAuiToolBarItem *m_publish_item; //wxAuiToolBarItem *m_publish_item;

View File

@@ -74,10 +74,12 @@ private:
std::unordered_set<std::string> m_system_filament_types_set; std::unordered_set<std::string> m_system_filament_types_set;
std::set<std::string> m_visible_printers; std::set<std::string> m_visible_printers;
CreateType m_create_type; CreateType m_create_type;
Button * m_button_create = nullptr;
Button * m_button_cancel = nullptr; Button * m_button_cancel = nullptr;
ComboBox * m_filament_vendor_combobox = nullptr; ComboBox * m_filament_vendor_combobox = nullptr;
::CheckBox * m_can_not_find_vendor_checkbox = nullptr; ::CheckBox * m_can_not_find_vendor_checkbox = nullptr;
ComboBox * m_filament_type_combobox = nullptr; ComboBox * m_filament_type_combobox = nullptr;
ComboBox * m_exist_vendor_combobox = nullptr;
ComboBox * m_filament_preset_combobox = nullptr; ComboBox * m_filament_preset_combobox = nullptr;
TextInput * m_filament_custom_vendor_input = nullptr; TextInput * m_filament_custom_vendor_input = nullptr;
wxGridSizer * m_filament_presets_sizer = nullptr; wxGridSizer * m_filament_presets_sizer = nullptr;

View File

@@ -16,27 +16,24 @@ namespace Slic3r
// This block is never executed at runtime. // This block is never executed at runtime.
static void _toolhead_translation_markers() static void _toolhead_translation_markers()
{ {
// Possible runtime values of tool_head_display_names, marked for extraction. // Dynamic toolhead display names from JSON config — xgettext cannot scan these
static const char *const markers[] = { L("Main Extruder"); L("Main extruder"); L("main extruder");
L("Main Extruder"), L("Main extruder"), L("main extruder"), L("Auxiliary Extruder"); L("Auxiliary extruder"); L("auxiliary extruder");
L("Auxiliary Extruder"), L("Auxiliary extruder"), L("auxiliary extruder"), L("Left Extruder"); L("Left extruder"); L("left extruder");
L("Left Extruder"), L("Left extruder"), L("left extruder"), L("Right Extruder"); L("Right extruder"); L("right extruder");
L("Right Extruder"), L("Right extruder"), L("right extruder"), L("Main Nozzle"); L("Main nozzle"); L("main nozzle");
L("Main Nozzle"), L("Main nozzle"), L("main nozzle"), L("Auxiliary Nozzle"); L("Auxiliary nozzle"); L("auxiliary nozzle");
L("Auxiliary Nozzle"), L("Auxiliary nozzle"), L("auxiliary nozzle"), L("Left Nozzle"); L("Left nozzle"); L("left nozzle");
L("Left Nozzle"), L("Left nozzle"), L("left nozzle"), L("Right Nozzle"); L("Right nozzle"); L("right nozzle");
L("Right Nozzle"), L("Right nozzle"), L("right nozzle"), L("Main Hotend"); L("Main hotend"); L("main hotend");
L("Main Hotend"), L("Main hotend"), L("main hotend"), L("Auxiliary Hotend"); L("Auxiliary hotend"); L("auxiliary hotend");
L("Auxiliary Hotend"), L("Auxiliary hotend"), L("auxiliary hotend"), L("Left Hotend"); L("Left hotend"); L("left hotend");
L("Left Hotend"), L("Left hotend"), L("left hotend"), L("Right Hotend"); L("Right hotend"); L("right hotend");
L("Right Hotend"), L("Right hotend"), L("right hotend"), // standalone position words (short_name=true runtime results)
// standalone position words (short_name=true runtime results) L("main"); L("auxiliary");
L("main"), L("auxiliary"), L("Main"); L("Auxiliary");
L("Main"), L("Auxiliary"), L("left"); L("right");
L("left"), L("right"), L("Left"); L("Right");
L("Left"), L("Right"),
};
(void) markers;
} }
std::string DevPrinterConfigUtil::m_resource_file_path = ""; std::string DevPrinterConfigUtil::m_resource_file_path = "";

View File

@@ -199,6 +199,7 @@ private:
void OnRefreshButton(wxCommandEvent& event); void OnRefreshButton(wxCommandEvent& event);
private: private:
int saveTimes{0};
wxBoxSizer* mainSizer{nullptr}; wxBoxSizer* mainSizer{nullptr};
wxPanel* textPanel{nullptr}; wxPanel* textPanel{nullptr};
wxBoxSizer* textSizer{nullptr}; wxBoxSizer* textSizer{nullptr};

View File

@@ -122,6 +122,7 @@ private:
private: private:
int m_ext_nozzle_id = -1; int m_ext_nozzle_id = -1;
int m_rack_nozzle_id = -1; int m_rack_nozzle_id = -1;
bool m_isRefreshFinish = false;
bool findNozzleImage = false; bool findNozzleImage = false;
NozzleStatus m_nozzle_status = NOZZLE_STATUS_DC; NozzleStatus m_nozzle_status = NOZZLE_STATUS_DC;
@@ -153,6 +154,7 @@ private:
Label* m_diameter_label; Label* m_diameter_label;
Label* m_flowtype_label; Label* m_flowtype_label;
Label* m_type_label; Label* m_type_label;
ScalableButton* m_error_button{ nullptr };
Label* m_sn_label; Label* m_sn_label;
Label* m_version_label; Label* m_version_label;

View File

@@ -266,7 +266,7 @@ void wgtDeviceNozzleRackSelect::OnNozzleItemSelected(wxCommandEvent &evt)
} }
auto *item = dynamic_cast<wgtDeviceNozzleRackNozzleItem *>(evt.GetEventObject()); auto *item = dynamic_cast<wgtDeviceNozzleRackNozzleItem *>(evt.GetEventObject());
if (auto ptr = m_nozzle_rack.lock(); item && ptr) { if (item; auto ptr = m_nozzle_rack.lock()) {
int to_select_pos_id = sGetNozzlePosId(item, m_toolhead_nozzle_l, m_toolhead_nozzle_r); int to_select_pos_id = sGetNozzlePosId(item, m_toolhead_nozzle_l, m_toolhead_nozzle_r);
if (to_select_pos_id > -1 && to_select_pos_id != GetSelectedNozzlePosID()) { if (to_select_pos_id > -1 && to_select_pos_id != GetSelectedNozzlePosID()) {
SetSelectedNozzle(ptr->GetNozzleSystem()->GetNozzleByPosId(to_select_pos_id)); SetSelectedNozzle(ptr->GetNozzleSystem()->GetNozzleByPosId(to_select_pos_id));

View File

@@ -72,6 +72,7 @@ public:
float m_model_z_offset{ 0.5f }; float m_model_z_offset{ 0.5f };
bool m_visible{ true }; bool m_visible{ true };
bool m_is_dark = false; bool m_is_dark = false;
bool m_fixed_screen_size{ false };
float m_scale_factor{ 1.0f }; float m_scale_factor{ 1.0f };
#if ENABLE_ACTUAL_SPEED_DEBUG #if ENABLE_ACTUAL_SPEED_DEBUG
ActualSpeedImguiWidget m_actual_speed_imgui_widget; ActualSpeedImguiWidget m_actual_speed_imgui_widget;

View File

@@ -327,6 +327,7 @@ private:
GLTexture m_icons_texture; GLTexture m_icons_texture;
bool m_icons_texture_dirty; bool m_icons_texture_dirty;
mutable GLTexture m_images_texture; mutable GLTexture m_images_texture;
mutable bool m_images_texture_dirty;
BackgroundTexture m_background_texture; BackgroundTexture m_background_texture;
GLTexture m_arrow_texture; GLTexture m_arrow_texture;
Layout m_layout; Layout m_layout;

View File

@@ -86,6 +86,7 @@ private:
boost::thread m_thread; boost::thread m_thread;
// Mutex and condition variable to synchronize m_thread with the UI thread. // Mutex and condition variable to synchronize m_thread with the UI thread.
std::mutex m_mutex; std::mutex m_mutex;
int m_generate_count;
// This map holds all translated description texts, so they can be easily referenced during layout calculations // This map holds all translated description texts, so they can be easily referenced during layout calculations
// etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect. // etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect.

View File

@@ -176,6 +176,7 @@ private:
// Use those values to disable selection of active extruders // Use those values to disable selection of active extruders
bool m_is_dark = false; bool m_is_dark = false;
bool is_osx{false};
int m_min_value; int m_min_value;
int m_max_value; int m_max_value;
int m_lower_value; int m_lower_value;
@@ -200,6 +201,10 @@ private:
void *m_one_layer_on_hover_id; void *m_one_layer_on_hover_id;
void *m_one_layer_off_id; void *m_one_layer_off_id;
void *m_one_layer_off_hover_id; void *m_one_layer_off_hover_id;
void* m_one_layer_on_light_id;
void* m_one_layer_on_hover_light_id;
void* m_one_layer_off_light_id;
void* m_one_layer_off_hover_light_id;
void* m_one_layer_on_dark_id; void* m_one_layer_on_dark_id;
void* m_one_layer_on_hover_dark_id; void* m_one_layer_on_hover_dark_id;
void* m_one_layer_off_dark_id; void* m_one_layer_off_dark_id;

View File

@@ -61,6 +61,7 @@ private:
::Button *m_button_year = nullptr; ::Button *m_button_year = nullptr;
::Button *m_button_month = nullptr; ::Button *m_button_month = nullptr;
::Button *m_button_all = nullptr; ::Button *m_button_all = nullptr;
::Label *m_switch_label = nullptr;
::StaticBox * m_type_panel = nullptr; ::StaticBox * m_type_panel = nullptr;
::Button * m_button_video = nullptr; ::Button * m_button_video = nullptr;

View File

@@ -86,8 +86,11 @@ private:
/* side tools */ /* side tools */
SideTools* m_side_tools{nullptr}; SideTools* m_side_tools{nullptr};
wxStaticBitmap* m_bitmap_printer_type;
wxStaticBitmap* m_bitmap_arrow; wxStaticBitmap* m_bitmap_arrow;
wxStaticText* m_staticText_printer_name;
wxStaticBitmap* m_bitmap_wifi_signal; wxStaticBitmap* m_bitmap_wifi_signal;
wxBoxSizer * m_side_tools_sizer;
SelectMachinePopup m_select_machine; SelectMachinePopup m_select_machine;
/* images */ /* images */
@@ -98,6 +101,7 @@ private:
wxBitmap m_printer_img; wxBitmap m_printer_img;
wxBitmap m_arrow_img; wxBitmap m_arrow_img;
int last_wifi_signal = -1;
int last_status; int last_status;
bool m_initialized { false }; bool m_initialized { false };
bool update_flag{false}; bool update_flag{false};

View File

@@ -88,6 +88,7 @@ private:
Button* m_task_name{ nullptr }; Button* m_task_name{ nullptr };
Button* m_status{ nullptr }; Button* m_status{ nullptr };
Button* m_action{ nullptr }; Button* m_action{ nullptr };
Button* m_stop_all_botton{nullptr};
// tip when no device // tip when no device
wxStaticText* m_tip_text{ nullptr }; wxStaticText* m_tip_text{ nullptr };

View File

@@ -79,6 +79,7 @@ private:
std::vector<wxBoxSizer *> m_row_col_boxsizer_list; std::vector<wxBoxSizer *> m_row_col_boxsizer_list;
std::vector<ButtonState*> m_result_icon_list; std::vector<ButtonState*> m_result_icon_list;
int m_last_cluster_num{-1}; int m_last_cluster_num{-1};
const int m_combox_width{50};
int m_combox_icon_width; int m_combox_icon_width;
int m_combox_icon_height; int m_combox_icon_height;
wxButton * m_image_button = nullptr; wxButton * m_image_button = nullptr;

View File

@@ -6470,7 +6470,7 @@ int PartPlateList::store_to_3mf_structure(PlateDataPtrs& plate_data_list, bool w
plate_data_item->filament_change_sequence = m_plate_list[i]->m_gcode_result->filament_change_sequence; plate_data_item->filament_change_sequence = m_plate_list[i]->m_gcode_result->filament_change_sequence;
plate_data_item->nozzle_change_sequence = m_plate_list[i]->m_gcode_result->nozzle_change_sequence; plate_data_item->nozzle_change_sequence = m_plate_list[i]->m_gcode_result->nozzle_change_sequence;
plate_data_item->optimal_assignment = m_plate_list[i]->m_gcode_result->optimal_assignment; plate_data_item->optimal_assignment = m_plate_list[i]->m_gcode_result->optimal_assignment;
plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->get_slice_result()->initial_layer_time); plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->cali_bboxes_data.first_layer_time);
Print *print = nullptr; Print *print = nullptr;
m_plate_list[i]->get_print((PrintBase **) &print, nullptr, nullptr); m_plate_list[i]->get_print((PrintBase **) &print, nullptr, nullptr);
if (print) { if (print) {

View File

@@ -128,6 +128,7 @@ private:
bool is_drag_mode(); bool is_drag_mode();
boost::shared_ptr<PrinterFileSystem> m_file_sys; boost::shared_ptr<PrinterFileSystem> m_file_sys;
bool m_file_sys_result{false};
std::string m_timestamp; std::string m_timestamp;
std::string m_tmp_path; std::string m_tmp_path;
std::vector<string> m_local_paths; std::vector<string> m_local_paths;

View File

@@ -33,6 +33,7 @@ class PhysicalPrinterDialog : public DPIDialog
Button* m_printhost_test_btn {nullptr}; Button* m_printhost_test_btn {nullptr};
Button* m_printhost_logout_btn {nullptr}; Button* m_printhost_logout_btn {nullptr};
Button* m_printhost_cafile_browse_btn {nullptr}; Button* m_printhost_cafile_browse_btn {nullptr};
Button* m_printhost_client_cert_browse_btn {nullptr};
Button* m_printhost_port_browse_btn {nullptr}; Button* m_printhost_port_browse_btn {nullptr};
RoundedRectangle* m_input_area {nullptr}; RoundedRectangle* m_input_area {nullptr};

View File

@@ -63,6 +63,7 @@ class ProjectPanel : public wxPanel
{ {
private: private:
std::atomic<bool> m_web_init_completed{false}; std::atomic<bool> m_web_init_completed{false};
bool m_reload_already = {false};
std::shared_ptr<std::atomic<bool>> m_reload_cancel_token{std::make_shared<std::atomic<bool>>(false)}; std::shared_ptr<std::atomic<bool>> m_reload_cancel_token{std::make_shared<std::atomic<bool>>(false)};
std::unique_ptr<boost::thread> m_reload_task; std::unique_ptr<boost::thread> m_reload_task;

View File

@@ -10,6 +10,8 @@ namespace Slic3r { namespace GUI {
class RecenterDialog : public DPIDialog class RecenterDialog : public DPIDialog
{ {
private: private:
wxStaticText* m_staticText_hint;
wxStaticBitmap* m_bitmap_home;
ScalableBitmap m_home_bmp; ScalableBitmap m_home_bmp;
wxString hint1; wxString hint1;
wxString hint2; wxString hint2;

View File

@@ -181,6 +181,8 @@ private:
PinCodePanel* m_panel_direct_connection{nullptr}; PinCodePanel* m_panel_direct_connection{nullptr};
wxWindow* m_placeholder_panel{nullptr}; wxWindow* m_placeholder_panel{nullptr};
HyperLink* m_hyperlink{nullptr}; // ORCA HyperLink* m_hyperlink{nullptr}; // ORCA
Label* m_ping_code_text{nullptr};
wxStaticBitmap* m_img_ping_code{nullptr};
wxBoxSizer * m_sizer_body{nullptr}; wxBoxSizer * m_sizer_body{nullptr};
wxBoxSizer * m_sizer_my_devices{nullptr}; wxBoxSizer * m_sizer_my_devices{nullptr};
wxBoxSizer * m_sizer_other_devices{nullptr}; wxBoxSizer * m_sizer_other_devices{nullptr};

View File

@@ -149,6 +149,7 @@ private:
ScalableBitmap* print_time{ nullptr }; ScalableBitmap* print_time{ nullptr };
wxStaticBitmap* weightimg{ nullptr }; wxStaticBitmap* weightimg{ nullptr };
ScalableBitmap* print_weight{ nullptr }; ScalableBitmap* print_weight{ nullptr };
wxBoxSizer* m_thumbnail_sizer{ nullptr };
ThumbnailPanel* m_thumbnail_panel{nullptr}; ThumbnailPanel* m_thumbnail_panel{nullptr};
wxPanel* m_panel_image{ nullptr }; wxPanel* m_panel_image{ nullptr };
wxBoxSizer* m_image_sizer{ nullptr }; wxBoxSizer* m_image_sizer{ nullptr };

View File

@@ -62,6 +62,7 @@ private:
bool m_is_in_sending_mode{ false }; bool m_is_in_sending_mode{ false };
bool m_is_rename_mode{ false }; bool m_is_rename_mode{ false };
bool enable_prepare_mode{ true }; bool enable_prepare_mode{ true };
bool m_need_adaptation_screen{ false };
bool m_export_3mf_cancel{ false }; bool m_export_3mf_cancel{ false };
bool m_is_canceled{ false }; bool m_is_canceled{ false };
bool m_tcp_try_connect{true}; bool m_tcp_try_connect{true};
@@ -80,6 +81,7 @@ private:
wxStaticBitmap* m_staticbitmap{ nullptr }; wxStaticBitmap* m_staticbitmap{ nullptr };
ThumbnailPanel* m_thumbnailPanel{ nullptr }; ThumbnailPanel* m_thumbnailPanel{ nullptr };
ComboBox* m_comboBox_printer{ nullptr }; ComboBox* m_comboBox_printer{ nullptr };
ComboBox* m_comboBox_bed{ nullptr };
Button* m_rename_button{ nullptr }; Button* m_rename_button{ nullptr };
Button* m_button_refresh{ nullptr }; Button* m_button_refresh{ nullptr };
Button* m_button_ensure{ nullptr }; Button* m_button_ensure{ nullptr };

View File

@@ -87,6 +87,7 @@ private:
std::unordered_map < uint32_t, std::vector<std::vector<FloatPoint>>> parts_triangles_; std::unordered_map < uint32_t, std::vector<std::vector<FloatPoint>>> parts_triangles_;
std::unordered_map < uint32_t, std::vector<std::vector<cv::Point>>> pick_parts_; std::unordered_map < uint32_t, std::vector<std::vector<cv::Point>>> pick_parts_;
std::unordered_map<uint32_t, PartState> parts_state_; std::unordered_map<uint32_t, PartState> parts_state_;
bool gl_inited_{false};
int zoom_percent_{100}; int zoom_percent_{100};
wxPoint offset_{0,0}; wxPoint offset_{0,0};
wxPoint drag_start_offset_{0,0}; wxPoint drag_start_offset_{0,0};

View File

@@ -307,6 +307,8 @@ private:
// wxBoxSizer * m_plate_combox_sizer = nullptr; // wxBoxSizer * m_plate_combox_sizer = nullptr;
wxBoxSizer * m_mode_combox_sizer = nullptr; wxBoxSizer * m_mode_combox_sizer = nullptr;
//wxStaticText * m_printer_title = nullptr; //wxStaticText * m_printer_title = nullptr;
wxStaticText * m_printer_device_name = nullptr;
wxStaticText * m_printer_is_map_title = nullptr;
CapsuleButton * m_colormap_btn = nullptr; CapsuleButton * m_colormap_btn = nullptr;
CapsuleButton * m_override_btn = nullptr; CapsuleButton * m_override_btn = nullptr;
@@ -324,6 +326,7 @@ private:
bool m_check_dirty_fialment = true; bool m_check_dirty_fialment = true;
bool m_expand_more_settings = true; bool m_expand_more_settings = true;
bool m_image_is_top = false;
const int LEFT_THUMBNAIL_SIZE_WIDTH = 100; const int LEFT_THUMBNAIL_SIZE_WIDTH = 100;
const int RIGHT_THUMBNAIL_SIZE_WIDTH = 300; const int RIGHT_THUMBNAIL_SIZE_WIDTH = 300;

View File

@@ -626,6 +626,8 @@ private:
bool m_rebuild_kinematics_page = false; bool m_rebuild_kinematics_page = false;
void update_input_shaper_menu(GCodeFlavor flavor); void update_input_shaper_menu(GCodeFlavor flavor);
ogStaticText* m_fff_print_host_upload_description_line {nullptr};
ogStaticText* m_sla_print_host_upload_description_line {nullptr};
std::vector<PageShp> m_pages_fff; std::vector<PageShp> m_pages_fff;
std::vector<PageShp> m_pages_sla; std::vector<PageShp> m_pages_sla;

View File

@@ -31,6 +31,7 @@ private:
wxStaticText *m_remaining_time_label; wxStaticText *m_remaining_time_label;
wxStaticText *m_explanation_label; wxStaticText *m_explanation_label;
wxButton *m_ok_button; wxButton *m_ok_button;
wxStaticBitmap *m_title_bitmap;
DECLARE_EVENT_TABLE() DECLARE_EVENT_TABLE()
}; };

View File

@@ -862,6 +862,7 @@ void MachineInfoPanel::update_ams_ext(MachineObject *obj)
if (new_extra_ams_ver != obj->new_ver_list.end()) if (new_extra_ams_ver != obj->new_ver_list.end())
has_new_version = true; has_new_version = true;
extra_ams_it->second.sw_new_ver;
if (has_new_version) { if (has_new_version) {
m_extra_ams_panel->m_ams_new_version_img->Show(); m_extra_ams_panel->m_ams_new_version_img->Show();
ver_text = new_extra_ams_ver->second.sw_ver; ver_text = new_extra_ams_ver->second.sw_ver;

View File

@@ -112,6 +112,7 @@ private:
AppConfig m_appconfig_new; AppConfig m_appconfig_new;
wxWebView *m_browser; wxWebView *m_browser;
wxButton * m_TestBtn;
wxString m_SectionName; wxString m_SectionName;

View File

@@ -125,6 +125,10 @@ private:
wxMenuItem* m_edit_undo; wxMenuItem* m_edit_undo;
wxMenuItem* m_edit_redo; wxMenuItem* m_edit_redo;
wxMenuItem* m_edit_mode; wxMenuItem* m_edit_mode;
wxMenuItem* m_scroll_line_up;
wxMenuItem* m_scroll_line_down;
wxMenuItem* m_scroll_page_up;
wxMenuItem* m_scroll_page_down;
wxMenuItem* m_script_string; wxMenuItem* m_script_string;
wxMenuItem* m_script_integer; wxMenuItem* m_script_integer;
wxMenuItem* m_script_double; wxMenuItem* m_script_double;

View File

@@ -114,6 +114,7 @@ bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag,
info.ctype = 0; info.ctype = 0;
info.material_colour = AMS_TRAY_DEFAULT_COL; info.material_colour = AMS_TRAY_DEFAULT_COL;
info.material_state = AMSCanType::AMS_CAN_TYPE_THIRDBRAND; info.material_state = AMSCanType::AMS_CAN_TYPE_THIRDBRAND;
wxColour(255, 255, 255);
} }
if (it->second->is_tray_info_ready() && obj->cali_version >= 0) { if (it->second->is_tray_info_ready() && obj->cali_version >= 0) {
@@ -170,6 +171,7 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
info.filament_id = ""; info.filament_id = "";
info.ctype = 0; info.ctype = 0;
info.material_colour = AMS_TRAY_DEFAULT_COL; info.material_colour = AMS_TRAY_DEFAULT_COL;
wxColour(255, 255, 255);
} }
info.material_state = AMSCanType::AMS_CAN_TYPE_VIRTUAL; info.material_state = AMSCanType::AMS_CAN_TYPE_VIRTUAL;
if (tray.is_tray_info_ready() && obj->cali_version >= 0) { if (tray.is_tray_info_ready() && obj->cali_version >= 0) {

View File

@@ -684,6 +684,7 @@ public:
private: private:
int m_nozzle_num = {1}; int m_nozzle_num = {1};
AMSRoadShowMode m_single_ext_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR}; AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR}; AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
bool m_selected = {false}; bool m_selected = {false};
@@ -692,6 +693,7 @@ private:
int m_right_road_length = {-1}; int m_right_road_length = {-1};
int m_passroad_width = {6}; int m_passroad_width = {6};
double m_radius = {4}; double m_radius = {4};
AMSPassRoadType m_pass_road_type = {AMSPassRoadType::AMS_ROAD_TYPE_NONE};
AMSPassRoadSTEP m_pass_road_left_step = {AMSPassRoadSTEP::AMS_ROAD_STEP_NONE}; AMSPassRoadSTEP m_pass_road_left_step = {AMSPassRoadSTEP::AMS_ROAD_STEP_NONE};
AMSPassRoadSTEP m_pass_road_right_step = {AMSPassRoadSTEP::AMS_ROAD_STEP_NONE}; AMSPassRoadSTEP m_pass_road_right_step = {AMSPassRoadSTEP::AMS_ROAD_STEP_NONE};
@@ -864,7 +866,11 @@ private:
AMSinfo m_info; AMSinfo m_info;
wxBoxSizer * sizer_can = {nullptr}; wxBoxSizer * sizer_can = {nullptr};
wxGridSizer* sizer_can_extra = { nullptr }; wxGridSizer* sizer_can_extra = { nullptr };
wxBoxSizer * sizer_humidity = { nullptr };
wxBoxSizer * sizer_item = { nullptr }; wxBoxSizer * sizer_item = { nullptr };
wxBoxSizer * sizer_can_middle = {nullptr};
wxBoxSizer * sizer_can_left = {nullptr};
wxBoxSizer * sizer_can_right = {nullptr};
AMSExtImage* m_ext_image = { nullptr }; //the ext image upon the ext ams AMSExtImage* m_ext_image = { nullptr }; //the ext image upon the ext ams
AMSExtText* m_ext_text = { nullptr }; //the ext text upon the ext ams AMSExtText* m_ext_text = { nullptr }; //the ext text upon the ext ams
}; };

View File

@@ -1014,12 +1014,8 @@ void FanControlPopupNew::init_names(MachineObject* obj) {
if (obj) { if (obj) {
const std::string& special_cooling_text = DevPrinterConfigUtil::get_fan_text(obj->printer_type, "special_cooling_text"); const std::string& special_cooling_text = DevPrinterConfigUtil::get_fan_text(obj->printer_type, "special_cooling_text");
if (!special_cooling_text.empty()) { if (!special_cooling_text.empty()) {
// Possible runtime values of special_cooling_text, marked for extraction. L("Cooling mode is suitable for printing PLA/PETG/TPU materials."); //some potential text, add i18n flags
static const char *const markers[] = { L("Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air.");
L("Cooling mode is suitable for printing PLA/PETG/TPU materials."),
L("Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air."),
};
(void) markers;
label_text[AIR_DUCT::AIR_DUCT_COOLING_FILT] = _L(special_cooling_text); label_text[AIR_DUCT::AIR_DUCT_COOLING_FILT] = _L(special_cooling_text);
} }
} }
@@ -1032,13 +1028,9 @@ wxString FanControlPopupNew::get_fan_func_name(int mode, int submode, AIR_FUN fu
const std::string& func_text = DevPrinterConfigUtil::get_fan_text(m_obj->printer_type, mode, (int)func, submode); const std::string& func_text = DevPrinterConfigUtil::get_fan_text(m_obj->printer_type, mode, (int)func, submode);
if (!func_text.empty()) if (!func_text.empty())
{ {
// Possible runtime values of func_text, marked for extraction. L_CONTEXT("Right(Aux)", "air_duct");
static const char *const markers[] = { L_CONTEXT("Right(Filter)", "air_duct");
L_CONTEXT("Right(Aux)", "air_duct"), L_CONTEXT("Left(Aux)", "air_duct");
L_CONTEXT("Right(Filter)", "air_duct"),
L_CONTEXT("Left(Aux)", "air_duct"),
};
(void) markers;
return _L_CONTEXT(func_text, "air_duct"); return _L_CONTEXT(func_text, "air_duct");
} }
} }

View File

@@ -109,6 +109,7 @@ public:
void decrease_fan_speeds(); void decrease_fan_speeds();
private: private:
int m_current_speeds; int m_current_speeds;
int m_target_speed;
int m_min_speeds; int m_min_speeds;
int m_max_speeds; int m_max_speeds;
ScalableBitmap m_bitmap_add; ScalableBitmap m_bitmap_add;

View File

@@ -204,6 +204,7 @@ private:
Label* m_caution; Label* m_caution;
wxTimer* m_refresh_timer {nullptr}; wxTimer* m_refresh_timer {nullptr};
size_t m_rack_event_token;
Button* m_cancel_btn; Button* m_cancel_btn;
Button* m_confirm_btn; Button* m_confirm_btn;
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,8 @@
#define slic3r_PluginAuditManager_hpp_ #define slic3r_PluginAuditManager_hpp_
#include <Python.h> #include <Python.h>
#include <memory>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector> #include <vector>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
@@ -21,31 +18,10 @@ struct AuditDecision {
struct AuditViolation { struct AuditViolation {
std::string plugin_key; std::string plugin_key;
std::string event_name; std::string event_name;
boost::filesystem::path path;
std::string reason; std::string reason;
}; };
// A filesystem root a plugin may access while an audit context is active. allow_write is
// false for a root that only grants reads (e.g. the bundled, shared resources folder) --
// a write-shaped event never matches such a root, even though a read-shaped one does.
struct AllowedRoot {
boost::filesystem::path path;
bool allow_write = true;
};
// The set of CPython audit events PluginAuditManager recognizes, grouped by the kind of
// operation they represent. None means the event isn't one audit_hook() acts on at all.
enum class AuditEventCategory {
None,
FsRead,
FsReadWrite,
FsCreate,
FsDelete,
Http,
Socket,
ProcessCreate,
Threading,
};
// Returns true if candidate resolves to a path inside allowed_root. // Returns true if candidate resolves to a path inside allowed_root.
// Uses weakly_canonical and component-wise comparison to reject traversal attacks. // Uses weakly_canonical and component-wise comparison to reject traversal attacks.
bool is_inside_allowed_root(const boost::filesystem::path& candidate, bool is_inside_allowed_root(const boost::filesystem::path& candidate,
@@ -73,15 +49,12 @@ public:
void clear_current_capability(); void clear_current_capability();
// --- allowed-roots registry --- // --- allowed-roots registry ---
// allow_write = false registers a read-only root: a read-shaped event inside it is allowed, void add_global_allowed_root(const boost::filesystem::path& root);
// but a write/create/delete-shaped event is not, so it falls through to the normal void add_scoped_allowed_root(const boost::filesystem::path& root);
// prompt-or-deny path instead.
void add_global_allowed_root(const boost::filesystem::path& root, bool allow_write = true);
void add_scoped_allowed_root(const boost::filesystem::path& root, bool allow_write = true);
// --- denied-filenames registry --- // --- denied-filenames registry ---
// Filenames a plugin may never touch, in any directory, regardless of the enclosing allowed // Filenames a plugin may never touch, in any directory, regardless of audit mode or
// root. A candidate is denied when its filename starts with a // enclosing allowed root. A candidate is denied when its filename starts with a
// registered name, so .bak/.tmp companions are covered by the same entry. // registered name, so .bak/.tmp companions are covered by the same entry.
// //
// The comparison is case-insensitive on every platform, unlike the _WIN32-only iequals // The comparison is case-insensitive on every platform, unlike the _WIN32-only iequals
@@ -100,60 +73,33 @@ public:
// 8.3 short name is out of scope (see the design doc). This blocks direct access only. // 8.3 short name is out of scope (see the design doc). This blocks direct access only.
bool is_denied_filename(const boost::filesystem::path& candidate) const; bool is_denied_filename(const boost::filesystem::path& candidate) const;
// --- denied-path-keyword registry --- // --- enforcement mode ---
// Keywords that categorically deny a path if ANY of its components (directory or file enum class AuditMode {
// name), not just the base name, contains one case-insensitively -- e.g. a "secrets" // Import/loading phase: allow reads anywhere, only block writes
// subfolder, a "certificates" folder, or a "conf"/"config" file anywhere the plugin can // outside allowed roots. Python needs to read stdlib modules
// otherwise reach, including inside an allowed root. This is intentionally broader and // during import and those are not inside plugin directories.
// fuzzier than the exact-name is_denied_filename registry: it exists to categorically rule Loading,
// out whole classes of sensitive paths (secrets, certificates, config) rather than name
// specific known files, at the cost of over-blocking an unrelated name that happens to
// contain the keyword -- the fail-safe direction, same rationale as is_denied_filename.
void add_denied_path_keyword(const std::string& keyword);
// The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact // Execution phase: block both reads and writes outside allowed
// same set without a live interpreter. // roots, plus subprocess/socket/ctypes.
static std::vector<std::string> default_denied_path_keywords(); Enforcing,
};
// True when any component of candidate's (canonicalized) path contains a registered void set_audit_mode(AuditMode mode);
// keyword, case-insensitively. AuditMode audit_mode() const;
bool is_denied_path_keyword(const boost::filesystem::path& candidate) const;
// is_denied_filename(candidate) || is_denied_path_keyword(candidate). Convenience for
// call sites that only need to know whether a path is categorically off-limits, not which
// specific rule fired.
bool is_denied_path(const boost::filesystem::path& candidate) const;
// --- policy checks --- // --- policy checks ---
// Shared core for every audited filesystem event. A plugin's own storage tree is fully // Shared core for every audited filesystem event. The deny list is consulted above the
// trusted; host-owned protected files remain denied, and broad keyword denies are evaluated // Loading-mode read exemption and above the allowed roots, so a denied filename is
// below the most-specific allowed root so Linux's .config parent does not block that root. // blocked even though every scope currently runs in Loading and the files in question
// sit inside data_dir(), which is itself a global allowed root.
AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write); AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write);
AuditDecision check_open(const std::string& path, const std::string& mode); AuditDecision check_open(const std::string& path, const std::string& mode);
// Ask the user to grant the requested filesystem-read paths. The request may originate on a
// plugin load worker, so the implementation marshals the modal dialog to the wx main thread.
// Returns true only when every missing path was granted and persisted; denial aborts the plugin
// load without adding a permission.
bool request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths);
void report_violation(const AuditViolation& violation); void report_violation(const AuditViolation& violation);
bool audit_denial_pending() const;
void clear_audit_denial();
void clear_last_violation(); void clear_last_violation();
bool last_violation(AuditViolation& violation) const; bool last_violation(AuditViolation& violation) const;
// --- call-site cascade cache ---
// A single plugin action often fires several nested CPython audit events as it passes
// through stdlib layers (urllib.request calling http.client calling socket, for example).
// Once the user approves one event, every stdlib frame still on the stack for that call
// is recorded here by (filename, function, first line) identity. A later event whose own
// ancestor chain still contains one of those frames is the same logical action seen from
// a deeper layer, so it is auto-approved instead of prompting again.
bool has_approved_ancestor(const std::string& plugin_key, const std::vector<std::string>& call_site_ids) const;
void record_approved_call_sites(const std::string& plugin_key, const std::vector<std::string>& call_site_ids);
bool verbose_events = true; bool verbose_events = true;
private: private:
@@ -163,25 +109,17 @@ private:
static int audit_hook(const char* event, PyObject* args, void* user_data); static int audit_hook(const char* event, PyObject* args, void* user_data);
// Check keyword denies while ignoring the components that make up an already-approved root. static thread_local std::string m_current_plugin_key;
// This keeps a root such as ~/.config/OrcaSlicer usable while still denying config/secret/ static thread_local std::string m_current_capability_name;
// certificate-like paths below that root. static thread_local AuditMode m_audit_mode;
bool is_denied_path_keyword_below_root(const boost::filesystem::path& candidate, static thread_local std::vector<boost::filesystem::path> m_scoped_allowed_roots;
const boost::filesystem::path& allowed_root) const; static thread_local bool m_has_last_violation;
static thread_local AuditViolation m_last_violation;
static thread_local std::string m_current_plugin_key; // mutable: is_denied_filename() is a const query that must lock.
static thread_local std::string m_current_capability_name;
static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots;
static thread_local bool m_audit_denial_pending;
static thread_local bool m_has_last_violation;
static thread_local AuditViolation m_last_violation;
// mutable: is_denied_filename() and has_approved_ancestor() are const queries that must lock.
mutable std::mutex m_mutex; mutable std::mutex m_mutex;
std::vector<AllowedRoot> m_global_allowed_roots; std::vector<boost::filesystem::path> m_global_allowed_roots;
std::vector<std::string> m_denied_filenames; std::vector<std::string> m_denied_filenames;
std::vector<std::string> m_denied_path_keywords;
std::unordered_map<std::string, std::unordered_set<std::string>> m_approved_call_sites; // plugin_key -> call-site ids
}; };
// RAII guard that sets the current plugin key and capability name, restoring the previous // RAII guard that sets the current plugin key and capability name, restoring the previous
@@ -192,7 +130,8 @@ class ScopedPluginAuditContext
public: public:
explicit ScopedPluginAuditContext( explicit ScopedPluginAuditContext(
const std::string& plugin_key, const std::string& plugin_key,
const std::string& capability_name = {}); const std::string& capability_name = {},
PluginAuditManager::AuditMode mode = PluginAuditManager::AuditMode::Loading);
~ScopedPluginAuditContext(); ~ScopedPluginAuditContext();
@@ -202,7 +141,8 @@ public:
private: private:
std::string m_previous_id; std::string m_previous_id;
std::string m_previous_capability; std::string m_previous_capability;
std::vector<AllowedRoot> m_previous_scoped_roots; PluginAuditManager::AuditMode m_previous_mode;
std::vector<boost::filesystem::path> m_previous_scoped_roots;
}; };
} // namespace Slic3r } // namespace Slic3r

View File

@@ -784,23 +784,6 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall
parsed.plugin_name = state["plugin_name"].get<std::string>(); parsed.plugin_name = state["plugin_name"].get<std::string>();
if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string()) if (state.contains("cloud_uuid") && state["cloud_uuid"].is_string())
parsed.cloud_uuid = state["cloud_uuid"].get<std::string>(); parsed.cloud_uuid = state["cloud_uuid"].get<std::string>();
if (state.contains("permissions") && state["permissions"].is_object()) {
const auto& permissions = state["permissions"];
auto read_string_list = [&permissions](const char* key, std::vector<std::string>& out) {
if (!permissions.contains(key) || !permissions[key].is_array())
return;
for (const auto& entry : permissions[key])
if (entry.is_string())
out.push_back(entry.get<std::string>());
};
read_string_list("fs_read", parsed.permissions.fs_read);
read_string_list("fs_readwrite", parsed.permissions.fs_readwrite);
read_string_list("network_http", parsed.permissions.network_http);
read_string_list("network_socket", parsed.permissions.network_socket);
read_string_list("process", parsed.permissions.process);
}
if (state.contains("enabled") && state["enabled"].is_boolean()) if (state.contains("enabled") && state["enabled"].is_boolean())
parsed.enabled = state["enabled"].get<bool>(); parsed.enabled = state["enabled"].get<bool>();
@@ -836,14 +819,6 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
if (!state.cloud_uuid.empty()) if (!state.cloud_uuid.empty())
json["cloud_uuid"] = state.cloud_uuid; json["cloud_uuid"] = state.cloud_uuid;
json["permissions"] = {
{"fs_read", state.permissions.fs_read},
{"fs_readwrite", state.permissions.fs_readwrite},
{"network_http", state.permissions.network_http},
{"network_socket", state.permissions.network_socket},
{"process", state.permissions.process},
};
nlohmann::json capabilities = nlohmann::json::array(); nlohmann::json capabilities = nlohmann::json::array();
for (const auto& [name, enabled] : state.capabilities) for (const auto& [name, enabled] : state.capabilities)
capabilities.push_back(nlohmann::json{{name, enabled}}); capabilities.push_back(nlohmann::json{{name, enabled}});
@@ -861,9 +836,6 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
const std::vector<std::pair<std::string, bool>>& capabilities) const std::vector<std::pair<std::string, bool>>& capabilities)
{ {
PluginInstallState state; PluginInstallState state;
// Loading a plugin updates its lifecycle/capability state, but must retain permissions granted
// during register_capabilities() or by a previous runtime audit prompt.
read_install_state(plugin_dir, state);
state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local"; state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local";
// Prefer the descriptor's recorded installed_version (the version fetched from the cloud // 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 // at install time, preserved across sidecar re-writes) so a stale manifest/PEP723 header
@@ -880,16 +852,10 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry) bool write_install_state(const boost::filesystem::path& plugin_dir, const PluginDescriptor& entry)
{ {
// Install-time writer: the package is not loaded, so its capabilities are not known yet and the // Install-time writer: the package is not loaded, so its capabilities are not known yet and the
// sidecar is (re)initialized to "auto-load, nothing disabled". This intentionally resets // sidecar is (re)initialized to "auto-load, nothing disabled". PluginManager writes the real
// permissions on a fresh install/reinstall. PluginManager writes the real per-capability flags // per-capability flags once the package is loaded, via the (dir, entry, enabled, capabilities)
// while preserving permissions once the package is loaded, via the overload above. // overload.
PluginInstallState state; return write_install_state(plugin_dir, entry, true, {});
state.installed_from = entry.is_cloud_plugin() ? "cloud" : "local";
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 = true;
return write_install_state(plugin_dir, state);
} }
bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error) bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginDescriptor& descriptor, std::string& error)

View File

@@ -82,23 +82,11 @@ inline nlohmann::json py_to_json(const pybind11::handle& o)
return py::str(o).cast<std::string>(); // fallback: str() return py::str(o).cast<std::string>(); // fallback: str()
} }
struct PluginPermissions
{
std::vector<std::string> fs_read;
std::vector<std::string> fs_readwrite;
std::vector<std::string> network_http;
std::vector<std::string> network_socket;
std::vector<std::string> process;
};
struct PluginInstallState { struct PluginInstallState {
std::string installed_from; // "local" | "cloud" std::string installed_from; // "local" | "cloud"
std::string installed_version; std::string installed_version;
std::string plugin_name; std::string plugin_name;
std::string cloud_uuid; // empty for local std::string cloud_uuid; // empty for local
PluginPermissions permissions;
bool enabled = true; bool enabled = true;
std::vector<std::pair<std::string, bool>> capabilities; // name -> enabled, ordered std::vector<std::pair<std::string, bool>> capabilities; // name -> enabled, ordered
}; };

View File

@@ -280,7 +280,7 @@ bool load(const PluginDescriptor& descriptor,
// (while the active plugin key is set), then instantiates each registered capability and caches // (while the active plugin key is set), then instantiates each registered capability and caches
// its get_name(). Returns one entry per capability. // its get_name(). Returns one entry per capability.
std::string bridge_error; std::string bridge_error;
auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, descriptor.plugin_key, bridge_error); auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, bridge_error);
if (!bridge_error.empty()) { if (!bridge_error.empty()) {
capabilities_found.clear(); capabilities_found.clear();
error = "Plugin registration failed: " + bridge_error; error = "Plugin registration failed: " + bridge_error;

View File

@@ -21,7 +21,6 @@
#include <chrono> #include <chrono>
#include <mutex> #include <mutex>
#include <slic3r/plugin/PluginConfig.hpp> #include <slic3r/plugin/PluginConfig.hpp>
#include <slic3r/plugin/PluginDescriptor.hpp>
#include <slic3r/plugin/PluginLoader.hpp> #include <slic3r/plugin/PluginLoader.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp> #include <slic3r/plugin/pluginTypes/script/ScriptPluginCapability.hpp>
@@ -623,20 +622,6 @@ std::shared_ptr<PluginCapabilityInterface> PluginManager::get_plugin_capability(
return nullptr; return nullptr;
} }
bool PluginManager::get_install_state(const std::string& plugin_key, PluginInstallState& install_state)
{
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor)) {
return false;
}
if (!read_install_state(boost::filesystem::path(descriptor.plugin_root), install_state)) {
return false;
}
return true;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────────────────── // ── Lifecycle ───────────────────────────────────────────────────────────────────────────────
bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const bool PluginManager::is_plugin_loaded(const std::string& plugin_key) const
@@ -925,8 +910,6 @@ void PluginManager::load_plugin_impl(const std::string& plugin_key, bool skip_de
if (!plugin_loader::load(descriptor, skip_deps, capabilities_to_enable, registry_precheck, plugin, error)) { if (!plugin_loader::load(descriptor, skip_deps, capabilities_to_enable, registry_precheck, plugin, error)) {
if (error == LOAD_CANCELLED) if (error == LOAD_CANCELLED)
return; // cancelled: nothing materialized survives, and no error is recorded return; // cancelled: nothing materialized survives, and no error is recorded
if (error.rfind("Plugin registration failed:", 0) == 0)
mark_plugin_install_state_disabled(plugin_key);
fail(std::move(error)); fail(std::move(error));
return; return;
} }
@@ -1152,51 +1135,6 @@ void PluginManager::write_loaded_plugin_install_state(const std::string& plugin_
write_install_state(boost::filesystem::path(descriptor.plugin_root), descriptor, /*enabled=*/true, capabilities); write_install_state(boost::filesystem::path(descriptor.plugin_root), descriptor, /*enabled=*/true, capabilities);
} }
void PluginManager::mark_plugin_install_state_disabled(const std::string& plugin_key)
{
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return;
const boost::filesystem::path root(descriptor.plugin_root);
PluginInstallState state;
if (!read_install_state(root, state)) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
}
state.enabled = false;
if (!write_install_state(root, state))
return;
std::lock_guard<std::mutex> lock(m_mutex);
if (Plugin* plugin = find_plugin_locked(plugin_key))
plugin->descriptor.enabled = false;
}
void PluginManager::revoke_plugin_permissions(const std::string& plugin_key)
{
std::lock_guard<std::mutex> state_lock(m_install_state_mutex);
PluginDescriptor descriptor;
if (!try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return;
const boost::filesystem::path root(descriptor.plugin_root);
PluginInstallState state;
if (!read_install_state(root, state)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to read install state for " << plugin_key;
return;
}
state.permissions = {};
if (!write_install_state(root, state))
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to revoke permissions for " << plugin_key;
}
// ── Callbacks ─────────────────────────────────────────────────────────────────────────────── // ── Callbacks ───────────────────────────────────────────────────────────────────────────────
void PluginManager::subscribe_on_load_callback(PluginLifecycleCompleteFn fn) void PluginManager::subscribe_on_load_callback(PluginLifecycleCompleteFn fn)
@@ -1416,11 +1354,6 @@ bool PluginManager::install_plugin(const boost::filesystem::path& filepath, Plug
return false; return false;
} }
// Every successful install may have replaced executable plugin code. Revoke any permissions
// associated with the previous package so the newly installed version must request them again.
if (!plugin_descriptor.plugin_key.empty())
revoke_plugin_permissions(plugin_descriptor.plugin_key);
if (!plugin_descriptor.plugin_key.empty()) if (!plugin_descriptor.plugin_key.empty())
clear_plugin_error(plugin_descriptor.plugin_key); clear_plugin_error(plugin_descriptor.plugin_key);
@@ -1808,7 +1741,6 @@ bool PluginManager::update_cloud_plugin(const std::string& plugin_key, std::stri
} }
clear_plugin_error(plugin_key); clear_plugin_error(plugin_key);
return true; return true;
} }

View File

@@ -159,8 +159,6 @@ public:
PluginCapabilityType type = PluginCapabilityType::Unknown, PluginCapabilityType type = PluginCapabilityType::Unknown,
bool only_enabled = true) const; bool only_enabled = true) const;
bool get_install_state(const std::string& plugin_key, PluginInstallState& install_state);
void load_plugin(const std::string& plugin_key, bool skip_deps = false, std::vector<std::string> capabilities_to_enable = {}); void load_plugin(const std::string& plugin_key, bool skip_deps = false, std::vector<std::string> capabilities_to_enable = {});
bool unload_plugin(const std::string& plugin_key); bool unload_plugin(const std::string& plugin_key);
void unload_all_plugins(); void unload_all_plugins();
@@ -257,9 +255,6 @@ private:
// Writes the sidecar for a loaded plugin (enabled=true plus the current per-capability flags). // Writes the sidecar for a loaded plugin (enabled=true plus the current per-capability flags).
void write_loaded_plugin_install_state(const std::string& plugin_key); void write_loaded_plugin_install_state(const std::string& plugin_key);
void mark_plugin_install_state_disabled(const std::string& plugin_key);
// Revoke permissions after a package replacement so the new package must request them again.
void revoke_plugin_permissions(const std::string& plugin_key);
bool finalize_cloud_plugin_removal(const PluginDescriptor& plugin, bool keep_local, std::string& error); 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 delete_installed_plugin_package(const PluginDescriptor& plugin, std::string& error);

View File

@@ -26,30 +26,25 @@
try { \ try { \
override_call; \ override_call; \
} catch (pybind11::error_already_set & err) { \ } catch (pybind11::error_already_set & err) { \
const bool _orca_audit_denial = ::Slic3r::PluginAuditManager::instance().audit_denial_pending(); \
if (_orca_audit_denial) \
::Slic3r::PluginAuditManager::instance().clear_current_plugin(); \
::Slic3r::log_python_exception_keep(err); \ ::Slic3r::log_python_exception_keep(err); \
if (_orca_audit_denial) \
::Slic3r::PluginAuditManager::instance().clear_audit_denial(); \
throw; \ throw; \
} }
// Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call, and publishes // Opens the plugin's filesystem audit scope for the duration of a C++ -> Python call, and publishes
// the calling capability's cached name so host APIs invoked from Python can tell which capability // the calling capability's cached name so host APIs invoked from Python can tell which capability
// they are serving. No-op without an audit plugin key. Declares a local `_orca_audit_scope`. // they are serving. No-op without an audit plugin key. Declares a local `_orca_audit_scope`.
#define ORCA_PY_AUDIT_SCOPE() \ #define ORCA_PY_AUDIT_SCOPE(mode) \
std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \ std::optional<::Slic3r::ScopedPluginAuditContext> _orca_audit_scope; \
if (const std::string& _orca_audit_key = this->audit_plugin_key(); !_orca_audit_key.empty()) \ if (const std::string& _orca_audit_key = this->audit_plugin_key(); !_orca_audit_key.empty()) \
_orca_audit_scope.emplace(_orca_audit_key, this->name()) _orca_audit_scope.emplace(_orca_audit_key, this->name(), mode)
#define ORCA_PY_OVERRIDE_AUDITED(audit_setup, override_macro, ret, base, name, ...) \ #define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \
do { \ do { \
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); \ ::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); \
::Slic3r::PythonGILState _orca_python_gil; \ ::Slic3r::PythonGILState _orca_python_gil; \
if (!_orca_python_gil) \ if (!_orca_python_gil) \
throw std::runtime_error("Python interpreter is shutting down"); \ throw std::runtime_error("Python interpreter is shutting down"); \
ORCA_PY_AUDIT_SCOPE(); \ ORCA_PY_AUDIT_SCOPE(mode); \
if (_orca_audit_scope) \ if (_orca_audit_scope) \
audit_setup(); \ audit_setup(); \
ORCA_PY_LOGGED_OVERRIDE_BODY(override_macro(ret, base, name, ##__VA_ARGS__)); \ ORCA_PY_LOGGED_OVERRIDE_BODY(override_macro(ret, base, name, ##__VA_ARGS__)); \
@@ -63,7 +58,7 @@ public:
std::string get_name() const override std::string get_name() const override
{ {
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name); ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name);
} }
// Config UI hooks. Available on every capability type, so they live here rather than in // Config UI hooks. Available on every capability type, so they live here rather than in
@@ -71,6 +66,7 @@ public:
bool has_config_ui() const override bool has_config_ui() const override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {}, [] {},
PYBIND11_OVERRIDE, PYBIND11_OVERRIDE,
bool, bool,
@@ -81,6 +77,7 @@ public:
std::string get_config_ui() const override std::string get_config_ui() const override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {}, [] {},
PYBIND11_OVERRIDE, PYBIND11_OVERRIDE,
std::string, std::string,
@@ -97,7 +94,7 @@ public:
// mistake), both fall back to the base's empty object rather than writing `"cap_config": null`. // mistake), both fall back to the base's empty object rather than writing `"cap_config": null`.
nlohmann::json get_default_config() const override nlohmann::json get_default_config() const override
{ {
ORCA_PY_AUDIT_SCOPE(); ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
try { try {
pybind11::gil_scoped_acquire gil; pybind11::gil_scoped_acquire gil;
pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config"); pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config");
@@ -120,17 +117,17 @@ public:
void on_load() override void on_load() override
{ {
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_load); ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_load);
} }
void on_unload() override void on_unload() override
{ {
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_unload); ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_unload);
} }
void on_cancelled() override void on_cancelled() override
{ {
ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled); ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
} }
}; };
@@ -142,7 +139,7 @@ public:
PluginCapabilityType get_type() const override PluginCapabilityType get_type() const override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface,
get_type); get_type);
} }
}; };

View File

@@ -87,12 +87,6 @@ void log_python_exception_keep(pybind11::error_already_set& err)
if (!gil) if (!gil)
return; return;
// Traceback output is host-owned work. In particular, the stderr tee opens the Python log
// file on every write. Keep that open outside the plugin audit context, otherwise an ordinary
// exception raised by a plugin can recursively trigger the filesystem permission dialog while
// its original exception is being reported.
ScopedPluginAuditContext audit_suppression("");
// Non-destructive: print the traceback to sys.stderr (tee'd to the session log) // 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++ // 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 // catchers can still read err.what() for the user-facing dialog. We must NOT use
@@ -628,6 +622,10 @@ bool PythonInterpreter::initialize()
else else
BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found"; BOOST_LOG_TRIVIAL(info) << "Bundled uv executable not found";
// Install the CPython audit hook for plugin policy enforcement.
// This is defense-in-depth: today it only inspects the `open` audit event
// and blocks writes outside the allowed roots; subprocess/socket/ctypes and
// other events are not yet handled. It is NOT a full security sandbox.
PluginAuditManager::instance().install_hook(); PluginAuditManager::instance().install_hook();
// Persist Python stderr (plugin tracebacks, including uncaught // Persist Python stderr (plugin tracebacks, including uncaught

View File

@@ -1,6 +1,5 @@
#include "PythonPluginBridge.hpp" #include "PythonPluginBridge.hpp"
#include <algorithm>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <exception> #include <exception>
#include <memory> #include <memory>
@@ -41,7 +40,6 @@ thread_local std::string g_active_plugin_key;
std::mutex g_registry_mutex; std::mutex g_registry_mutex;
std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities; std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities;
std::unordered_map<std::string, py::object> g_pending_package; std::unordered_map<std::string, py::object> g_pending_package;
std::unordered_map<std::string, std::vector<std::string>> g_pending_fs_read_permissions;
struct PluginInstanceHandle struct PluginInstanceHandle
{ {
// The C++ plugin interface points into a Python object. Keep both alive through one // The C++ plugin interface points into a Python object. Keep both alive through one
@@ -79,8 +77,6 @@ void discard_pending_capture_without_python(const std::string& plugin_key)
(void) package->second.release(); (void) package->second.release();
g_pending_package.erase(package); g_pending_package.erase(package);
} }
g_pending_fs_read_permissions.erase(plugin_key);
} }
} // namespace } // namespace
@@ -105,38 +101,34 @@ void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
// for this same entry path. // for this same entry path.
g_pending_capabilities.erase(plugin_key); g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key); g_pending_package.erase(plugin_key);
g_pending_fs_read_permissions.erase(plugin_key);
} }
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by // From now until finalize/cancel, @orca.plugin and register_capability() calls made by
// Python code on this thread are attributed to this plugin. // Python code on this thread are attributed to this plugin.
g_active_plugin_key = plugin_key; g_active_plugin_key = plugin_key;
} }
std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& capture_key, std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& plugin_key, std::string& error)
const std::string& plugin_key,
std::string& error)
{ {
PythonGILState gil; PythonGILState gil;
if (!gil) { if (!gil) {
error = "Python interpreter is shutting down"; error = "Python interpreter is shutting down";
return {}; return {};
} }
BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << capture_key; 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 // 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 // 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 // capability class, and register_capability() needs g_active_plugin_key to know which
// pending bucket to append to. // pending bucket to append to.
{ {
auto clear_active_key = [&capture_key]() { auto clear_active_key = [&plugin_key]() {
if (g_active_plugin_key == capture_key) if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear(); g_active_plugin_key.clear();
}; };
auto discard_pending_for_key = [&capture_key]() { auto discard_pending_for_key = [&plugin_key]() {
std::lock_guard<std::mutex> lock(g_registry_mutex); std::lock_guard<std::mutex> lock(g_registry_mutex);
g_pending_capabilities.erase(capture_key); g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(capture_key); g_pending_package.erase(plugin_key);
g_pending_fs_read_permissions.erase(capture_key);
}; };
try { try {
@@ -146,7 +138,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
py::object package_cls; py::object package_cls;
{ {
std::lock_guard<std::mutex> lock(g_registry_mutex); std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_package.find(capture_key); auto it = g_pending_package.find(plugin_key);
if (it != g_pending_package.end()) { if (it != g_pending_package.end()) {
package_cls = it->second; package_cls = it->second;
g_pending_package.erase(it); g_pending_package.erase(it);
@@ -165,33 +157,6 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// are kept. // are kept.
py::object package = package_cls(); py::object package = package_cls();
package.attr("register_capabilities")(); package.attr("register_capabilities")();
// Permission declarations are collected while register_capabilities() runs so the
// plugin can describe its needs without touching wx from the Python load worker. Ask
// only after registration returns, before capability instances are materialized.
std::vector<std::string> fs_read_permissions;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_fs_read_permissions.find(capture_key);
if (it != g_pending_fs_read_permissions.end()) {
fs_read_permissions = std::move(it->second);
g_pending_fs_read_permissions.erase(it);
}
}
if (!fs_read_permissions.empty()) {
bool granted = false;
{
py::gil_scoped_release release;
granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions);
}
if (!granted) {
error = "Plugin filesystem read permission request denied";
BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key;
discard_pending_for_key();
clear_active_key();
return {};
}
}
} catch (py::error_already_set& err) { } catch (py::error_already_set& err) {
log_python_exception_keep(err); log_python_exception_keep(err);
error = err.what(); error = err.what();
@@ -215,7 +180,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
std::vector<py::object> classes; std::vector<py::object> classes;
{ {
std::lock_guard<std::mutex> lock(g_registry_mutex); std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_capabilities.find(capture_key); auto it = g_pending_capabilities.find(plugin_key);
if (it != g_pending_capabilities.end()) { if (it != g_pending_capabilities.end()) {
classes = std::move(it->second); classes = std::move(it->second);
g_pending_capabilities.erase(it); g_pending_capabilities.erase(it);
@@ -224,10 +189,10 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// Registration is complete. Later register_capability() calls should fail instead of // Registration is complete. Later register_capability() calls should fail instead of
// accidentally attaching themselves to this plugin. // accidentally attaching themselves to this plugin.
if (g_active_plugin_key == capture_key) if (g_active_plugin_key == plugin_key)
g_active_plugin_key.clear(); g_active_plugin_key.clear();
BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << capture_key; BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << plugin_key;
std::vector<CapturedCapability> capabilities; std::vector<CapturedCapability> capabilities;
capabilities.reserve(classes.size()); capabilities.reserve(classes.size());
@@ -239,7 +204,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
py::object instance = cls(); py::object instance = cls();
if (!py::isinstance<PluginCapabilityInterface>(instance)) { if (!py::isinstance<PluginCapabilityInterface>(instance)) {
error = "Registered capability must inherit from a PluginCapability base"; error = "Registered capability must inherit from a PluginCapability base";
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << capture_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << plugin_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -247,7 +212,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
auto capability_iface = instance.cast<std::shared_ptr<PluginCapabilityInterface>>(); auto capability_iface = instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
if (!capability_iface) { if (!capability_iface) {
error = "Failed to cast Python capability to PluginCapabilityInterface"; error = "Failed to cast Python capability to PluginCapabilityInterface";
BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << capture_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << plugin_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -261,7 +226,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// here is a hard error that rejects the whole plugin capture. // here is a hard error that rejects the whole plugin capture.
if (name.find(';') != std::string::npos) { if (name.find(';') != std::string::npos) {
error = "Capability name must not contain ';': " + name; error = "Capability name must not contain ';': " + name;
BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << capture_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << plugin_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -281,12 +246,12 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// log the traceback here. GIL is held for the duration of finalize_plugin_capture. // log the traceback here. GIL is held for the duration of finalize_plugin_capture.
log_python_exception_keep(err); log_python_exception_keep(err);
error = err.what(); error = err.what();
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << capture_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << plugin_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
error = ex.what(); error = ex.what();
BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << capture_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << plugin_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -314,7 +279,6 @@ void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
// may already have registered under this key. // may already have registered under this key.
g_pending_capabilities.erase(plugin_key); g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key); g_pending_package.erase(plugin_key);
g_pending_fs_read_permissions.erase(plugin_key);
} }
if (g_active_plugin_key == plugin_key) if (g_active_plugin_key == plugin_key)
@@ -341,7 +305,6 @@ void PythonPluginBridge::clear_pending_captures()
(void) pkg.release(); (void) pkg.release();
} }
g_pending_package.clear(); g_pending_package.clear();
g_pending_fs_read_permissions.clear();
g_active_plugin_key.clear(); g_active_plugin_key.clear();
return; return;
} }
@@ -350,7 +313,6 @@ void PythonPluginBridge::clear_pending_captures()
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)"; BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
g_pending_capabilities.clear(); g_pending_capabilities.clear();
g_pending_package.clear(); g_pending_package.clear();
g_pending_fs_read_permissions.clear();
g_active_plugin_key.clear(); g_active_plugin_key.clear();
} }
@@ -488,27 +450,6 @@ void bind_python_api(pybind11::module_& m)
}, },
R"pbdoc(Register a PluginCapability subclass while OrcaSlicer loads your module.)pbdoc"); R"pbdoc(Register a PluginCapability subclass while OrcaSlicer loads your module.)pbdoc");
m.def(
"request_permissions",
[](const std::vector<std::string>& fs_read) {
if (g_active_plugin_key.empty())
throw py::value_error("request_permissions() called outside plugin discovery context");
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto& requested = g_pending_fs_read_permissions[g_active_plugin_key];
for (const std::string& path : fs_read) {
if (path.empty())
throw py::value_error("request_permissions(fs_read=...) does not accept empty paths");
if (std::find(requested.begin(), requested.end(), path) == requested.end())
requested.push_back(path);
}
},
py::arg("fs_read") = std::vector<std::string>{},
R"pbdoc(Request filesystem read permissions while OrcaSlicer loads your module.
The host presents the request to the user after register_capabilities() returns. Denying the
request aborts the plugin load.)pbdoc");
m.def("plugin", [](py::object cls) { m.def("plugin", [](py::object cls) {
if (g_active_plugin_key.empty()) if (g_active_plugin_key.empty())
throw py::value_error("@orca.plugin used outside plugin discovery context"); throw py::value_error("@orca.plugin used outside plugin discovery context");

View File

@@ -31,7 +31,7 @@ public:
// Returns one CapturedCapability per capability, or an empty vector on failure // Returns one CapturedCapability per capability, or an empty vector on failure
// (error message populated). // (error message populated).
std::vector<CapturedCapability> finalize_plugin_capture( std::vector<CapturedCapability> finalize_plugin_capture(
const std::string& capture_key, const std::string& plugin_key, std::string& error); const std::string& plugin_key, std::string& error);
// Clear any pending registrations for the key. Safe to call when import fails. // Clear any pending registrations for the key. Safe to call when import fails.
void cancel_plugin_capture(const std::string& plugin_key); void cancel_plugin_capture(const std::string& plugin_key);

View File

@@ -16,6 +16,7 @@ public:
std::string get_icon() override std::string get_icon() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {}, [] {},
PYBIND11_OVERRIDE, PYBIND11_OVERRIDE,
std::string, std::string,
@@ -26,6 +27,7 @@ public:
std::string get_ui() override std::string get_ui() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {}, [] {},
PYBIND11_OVERRIDE_PURE, PYBIND11_OVERRIDE_PURE,
std::string, std::string,
@@ -40,7 +42,7 @@ public:
if (!gil) if (!gil)
throw std::runtime_error("Python interpreter is shutting down"); throw std::runtime_error("Python interpreter is shutting down");
ORCA_PY_AUDIT_SCOPE(); ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
pybind11::function override = pybind11::get_override(static_cast<PagesPluginCapability*>(this), "on_message"); pybind11::function override = pybind11::get_override(static_cast<PagesPluginCapability*>(this), "on_message");
if (!override) if (!override)

View File

@@ -16,206 +16,206 @@ public:
AgentInfo get_agent_info() override AgentInfo get_agent_info() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability,
get_agent_info); get_agent_info);
} }
int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override 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( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id,
dev_ip, username, password, use_ssl); dev_ip, username, password, use_ssl);
} }
int disconnect_printer() override int disconnect_printer() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer); ::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 int send_message(std::string dev_id, std::string json_str, int qos, int flag) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id,
json_str, qos, flag); json_str, qos, flag);
} }
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer,
dev_id, json_str, qos, flag); dev_id, json_str, qos, flag);
} }
bool start_discovery(bool start, bool sending) override bool start_discovery(bool start, bool sending) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start,
sending); sending);
} }
int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override int bind_detect(std::string dev_ip, std::string sec_link, detectResult& detect) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip,
sec_link, detect); sec_link, detect);
} }
std::string get_user_selected_machine() override std::string get_user_selected_machine() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability,
get_user_selected_machine); get_user_selected_machine);
} }
int set_user_selected_machine(std::string dev_id) override int set_user_selected_machine(std::string dev_id) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
set_user_selected_machine, dev_id); set_user_selected_machine, dev_id);
} }
int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override int start_send_gcode_to_sdcard(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_send_gcode_to_sdcard, params, update_fn, cancel_fn, wait_fn); 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 int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print,
params, update_fn, cancel_fn); params, update_fn, cancel_fn);
} }
FilamentSyncMode get_filament_sync_mode() const override FilamentSyncMode get_filament_sync_mode() const override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability,
get_filament_sync_mode); get_filament_sync_mode);
} }
bool fetch_filament_info(std::string dev_id) override bool fetch_filament_info(std::string dev_id) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id);
} }
int check_cert() override int check_cert() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert);
} }
void install_device_cert(std::string dev_id, bool lan_only) override void install_device_cert(std::string dev_id, bool lan_only) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id,
lan_only); lan_only);
} }
int ping_bind(std::string ping_code) override int ping_bind(std::string ping_code) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code); ::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 dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override int bind(std::string dev_ip, std::string dev_id, std::string dev_model, std::string sec_link, std::string timezone, bool improved, OnUpdateStatusFn update_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id,
dev_model, sec_link, timezone, improved, update_fn); dev_model, sec_link, timezone, improved, update_fn);
} }
int unbind(std::string dev_id) override int unbind(std::string dev_id) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id); ::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 int start_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params,
update_fn, cancel_fn, wait_fn); update_fn, cancel_fn, wait_fn);
} }
int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override int start_local_print_with_record(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn, OnWaitFn wait_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability,
start_local_print_with_record, params, update_fn, cancel_fn, wait_fn); 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 int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params,
update_fn, cancel_fn); update_fn, cancel_fn);
} }
int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override int get_hms_snapshot(std::string dev_id, std::string file_name, std::function<void(std::string, int)> callback) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id,
file_name, callback); file_name, callback);
} }
int set_server_callback(OnServerErrFn fn) override int set_server_callback(OnServerErrFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn);
} }
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn);
} }
int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override int set_on_printer_connected_fn(OnPrinterConnectedFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn,
fn); fn);
} }
int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override int set_on_subscribe_failure_fn(GetSubscribeFailureFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn, ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn,
fn); fn);
} }
int set_on_message_fn(OnMessageFn fn) override int set_on_message_fn(OnMessageFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn);
} }
int set_on_user_message_fn(OnMessageFn fn) override int set_on_user_message_fn(OnMessageFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn);
} }
int set_on_local_connect_fn(OnLocalConnectedFn fn) override int set_on_local_connect_fn(OnLocalConnectedFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn);
} }
int set_on_local_message_fn(OnMessageFn fn) override int set_on_local_message_fn(OnMessageFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn); ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn);
} }
int set_queue_on_main_fn(QueueOnMainFn fn) override int set_queue_on_main_fn(QueueOnMainFn fn) override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
[] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn); ::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 // request_bind_ticket returns its ticket through a std::string* out-param, which pybind11
@@ -223,7 +223,7 @@ public:
// returns a (result, ticket) tuple, which we unpack into the int result and the out-param. // returns a (result, ticket) tuple, which we unpack into the int result and the out-param.
int request_bind_ticket(std::string* ticket) override int request_bind_ticket(std::string* ticket) override
{ {
ORCA_PY_AUDIT_SCOPE(); ORCA_PY_AUDIT_SCOPE(::Slic3r::PluginAuditManager::AuditMode::Loading);
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); ::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
::Slic3r::PythonGILState gil; ::Slic3r::PythonGILState gil;
if (!gil) if (!gil)

View File

@@ -13,6 +13,7 @@ public:
ExecutionResult execute() override ExecutionResult execute() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[] {}, [] {},
PYBIND11_OVERRIDE_PURE, PYBIND11_OVERRIDE_PURE,
ExecutionResult, ExecutionResult,

View File

@@ -10,6 +10,7 @@ public:
using PyPluginCommonTrampoline<SlicingPipelinePluginCapability>::PyPluginCommonTrampoline; using PyPluginCommonTrampoline<SlicingPipelinePluginCapability>::PyPluginCommonTrampoline;
ExecutionResult execute(SlicingPipelineContext& ctx) override { ExecutionResult execute(SlicingPipelineContext& ctx) override {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading,
[&]{ [&]{
// At Step.psGCodePostProcess the plugin edits the exported G-code file, which lives // At Step.psGCodePostProcess the plugin edits the exported G-code file, which lives
// outside data_dir() (a temp/output folder), so writing to it would otherwise be // outside data_dir() (a temp/output folder), so writing to it would otherwise be

View File

@@ -34,24 +34,4 @@ struct ScopedDataDir
ScopedDataDir& operator=(const ScopedDataDir&) = delete; ScopedDataDir& operator=(const ScopedDataDir&) = delete;
}; };
// Point resources_dir() at a throwaway directory for the lifetime of a test and restore the
// previous value afterwards, mirroring ScopedDataDir.
struct ScopedResourcesDir
{
ScopedTemporaryDir tmp;
boost::filesystem::path dir;
std::string previous;
explicit ScopedResourcesDir(const std::string& tag)
: tmp("orca-" + tag), dir(tmp.path()), previous(resources_dir())
{
set_resources_dir(dir.string());
}
~ScopedResourcesDir() { set_resources_dir(previous); }
ScopedResourcesDir(const ScopedResourcesDir&) = delete;
ScopedResourcesDir& operator=(const ScopedResourcesDir&) = delete;
};
} // namespace Slic3r } // namespace Slic3r

View File

@@ -27,16 +27,6 @@ void seed_denied_names()
mgr.add_denied_filename(name); mgr.add_denied_filename(name);
} }
// Seed the keyword registry with the same list install_hook() uses. Same rationale as
// seed_denied_names(): a process-singleton registry, seeded from the single shared source so
// production and tests cannot drift apart.
void seed_denied_keywords()
{
PluginAuditManager& mgr = PluginAuditManager::instance();
for (const auto& keyword : PluginAuditManager::default_denied_path_keywords())
mgr.add_denied_path_keyword(keyword);
}
} // namespace } // namespace
TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit]") TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit]")
@@ -91,7 +81,7 @@ TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit
} }
} }
TEST_CASE("Plugin audit deny beats allowed roots", "[audit]") TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption", "[audit]")
{ {
ScopedDataDir data_dir_guard("plugin-audit-deny"); ScopedDataDir data_dir_guard("plugin-audit-deny");
seed_denied_names(); seed_denied_names();
@@ -101,8 +91,8 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
// config and the token would otherwise be reachable simply by living inside it. // config and the token would otherwise be reachable simply by living inside it.
mgr.add_global_allowed_root(data_dir()); mgr.add_global_allowed_root(data_dir());
// Enter a plugin context. The deny must hold even inside a globally allowed root. // Enter a plugin context. The deny must hold in Loading mode, which every scope runs in.
ScopedPluginAuditContext ctx("test_plugin", ""); ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading);
const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf"); const fs::path conf = fs::path(data_dir()) / (SLIC3R_APP_KEY ".conf");
const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME; const fs::path token = fs::path(data_dir()) / secret_constants::USER_SECRET_FILENAME;
@@ -113,13 +103,6 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
CHECK(decision.allowed); CHECK(decision.allowed);
} }
SECTION("a file outside the allowed root is blocked for reads as well as writes")
{
AuditDecision decision = mgr.check_open((fs::path(data_dir()).parent_path() / "outside.txt").string(), "r");
CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "outside allowed root");
}
SECTION("writing the app config is blocked despite data_dir() being allowed") SECTION("writing the app config is blocked despite data_dir() being allowed")
{ {
AuditDecision decision = mgr.check_open(conf.string(), "w"); AuditDecision decision = mgr.check_open(conf.string(), "w");
@@ -127,14 +110,16 @@ TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
CHECK(decision.reason == "denied filename"); CHECK(decision.reason == "denied filename");
} }
SECTION("reading the app config is blocked despite the allowed root") SECTION("reading the app config is blocked even though Loading exempts reads")
{ {
// Without the deny, a read in Loading mode short-circuits to allow. The deny sits above
// that exemption, so this must still be blocked.
AuditDecision decision = mgr.check_open(conf.string(), "r"); AuditDecision decision = mgr.check_open(conf.string(), "r");
CHECK_FALSE(decision.allowed); CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "denied filename"); CHECK(decision.reason == "denied filename");
} }
SECTION("reading the cloud refresh token is blocked") SECTION("reading the cloud refresh token is blocked in Loading mode")
{ {
AuditDecision decision = mgr.check_open(token.string(), "r"); AuditDecision decision = mgr.check_open(token.string(), "r");
CHECK_FALSE(decision.allowed); CHECK_FALSE(decision.allowed);
@@ -165,7 +150,7 @@ TEST_CASE("Plugin audit deny beats a plugin's own scoped root", "[audit]")
const fs::path plugin_dir = fs::path(data_dir()) / "plugins" / "test_plugin"; const fs::path plugin_dir = fs::path(data_dir()) / "plugins" / "test_plugin";
fs::create_directories(plugin_dir); fs::create_directories(plugin_dir);
ScopedPluginAuditContext ctx("test_plugin", ""); ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading);
mgr.add_scoped_allowed_root(plugin_dir); mgr.add_scoped_allowed_root(plugin_dir);
SECTION("the plugin's own non-denied file opens for read and write") SECTION("the plugin's own non-denied file opens for read and write")
@@ -184,43 +169,6 @@ TEST_CASE("Plugin audit deny beats a plugin's own scoped root", "[audit]")
} }
} }
TEST_CASE("Plugin audit allows an allowed root below a configuration directory", "[audit][Regression]")
{
ScopedDataDir data_dir_guard("plugin-audit-config-parent");
seed_denied_names();
seed_denied_keywords();
// This mirrors Linux's default ~/.config/OrcaSlicer layout without depending on the host's
// actual home directory or XDG_CONFIG_HOME setting.
const fs::path linux_data_dir = data_dir_guard.dir / ".config" / "OrcaSlicer";
const fs::path plugin_storage = linux_data_dir / "orca_plugins" / "plugin_data" / "test_plugin";
fs::create_directories(plugin_storage);
set_data_dir(linux_data_dir.string());
PluginAuditManager& mgr = PluginAuditManager::instance();
mgr.add_global_allowed_root(data_dir());
ScopedPluginAuditContext ctx("test_plugin", "");
SECTION("plugin storage remains accessible")
{
CHECK(mgr.check_open((plugin_storage / "state.json").string(), "w").allowed);
}
SECTION("all plugin storage names remain accessible")
{
CHECK(mgr.check_open((plugin_storage / "config" / "settings.json").string(), "r").allowed);
CHECK(mgr.check_open((plugin_storage / "secret" / "token.txt").string(), "w").allowed);
CHECK(mgr.check_open((plugin_storage / (SLIC3R_APP_KEY ".conf")).string(), "r").allowed);
}
SECTION("host-owned protected files remain denied")
{
const AuditDecision decision = mgr.check_open((linux_data_dir / (SLIC3R_APP_KEY ".conf")).string(), "r");
CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "denied filename");
}
}
TEST_CASE("Plugin audit does not constrain non-plugin code", "[audit]") TEST_CASE("Plugin audit does not constrain non-plugin code", "[audit]")
{ {
ScopedDataDir data_dir_guard("plugin-audit-noplugin"); ScopedDataDir data_dir_guard("plugin-audit-noplugin");
@@ -237,139 +185,3 @@ TEST_CASE("Plugin audit does not constrain non-plugin code", "[audit]")
CHECK(mgr.check_open(conf.string(), "w").allowed); CHECK(mgr.check_open(conf.string(), "w").allowed);
CHECK(mgr.check_open(conf.string(), "r").allowed); CHECK(mgr.check_open(conf.string(), "r").allowed);
} }
TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword", "[audit]")
{
seed_denied_keywords();
const PluginAuditManager& mgr = PluginAuditManager::instance();
SECTION("a 'secrets' directory component is denied")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/secrets/api_key.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/secret/token.txt")));
}
SECTION("a 'certificate(s)' directory component is denied")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/cert/slicer_base64.cer")));
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/certificates/ca.pem")));
}
SECTION("a 'conf'/'config' directory or file component is denied")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/conf/settings.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/config/settings.json")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/plugin.conf")));
}
SECTION("matching is case-insensitive")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/SECRETS/token.txt")));
CHECK(mgr.is_denied_path_keyword(fs::path("/resources/CertBundle/ca.pem")));
CHECK(mgr.is_denied_path_keyword(fs::path("/plugin/CONFIG.JSON")));
}
SECTION("matching is not limited to the base name -- any ancestor component counts")
{
CHECK(mgr.is_denied_path_keyword(fs::path("/data/secrets/nested/deep/file.txt")));
}
SECTION("an unrelated path is not denied")
{
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/output/model.gcode")));
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path("/plugin/storage/state.json")));
}
SECTION("an empty path is not denied")
{
CHECK_FALSE(mgr.is_denied_path_keyword(fs::path()));
}
}
TEST_CASE("Plugin audit is_denied_path combines the filename and keyword registries", "[audit]")
{
seed_denied_names();
seed_denied_keywords();
const PluginAuditManager& mgr = PluginAuditManager::instance();
SECTION("a filename-registry match is denied")
{
CHECK(mgr.is_denied_path(fs::path(SLIC3R_APP_KEY ".conf")));
}
SECTION("a keyword-registry match is denied")
{
CHECK(mgr.is_denied_path(fs::path("/plugin/secrets/token.txt")));
}
SECTION("a path matching neither registry is not denied")
{
CHECK_FALSE(mgr.is_denied_path(fs::path("/plugin/output/model.gcode")));
}
}
TEST_CASE("Plugin audit a read-only allowed root blocks writes but not reads", "[audit]")
{
ScopedDataDir data_dir_guard("plugin-audit-readonly");
ScopedResourcesDir resources_dir_guard("plugin-audit-readonly-resources");
seed_denied_names();
seed_denied_keywords();
PluginAuditManager& mgr = PluginAuditManager::instance();
mgr.add_global_allowed_root(resources_dir(), /*allow_write=*/false);
ScopedPluginAuditContext ctx("test_plugin", "");
const fs::path readonly_file = fs::path(resources_dir()) / "profiles" / "vendor.json";
SECTION("a read inside the read-only root is allowed")
{
CHECK(mgr.check_open(readonly_file.string(), "r").allowed);
}
SECTION("a write inside the read-only root is blocked")
{
AuditDecision decision = mgr.check_open(readonly_file.string(), "w");
CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "outside allowed root");
}
SECTION("a create inside the read-only root is blocked")
{
AuditDecision decision = mgr.check_path_access(readonly_file, /*is_write=*/true);
CHECK_FALSE(decision.allowed);
}
SECTION("the bundled cert underneath the read-only root is denied even for reads")
{
const fs::path cert = fs::path(resources_dir()) / "cert" / "slicer_base64.cer";
AuditDecision decision = mgr.check_open(cert.string(), "r");
CHECK_FALSE(decision.allowed);
CHECK(decision.reason == "denied path keyword");
}
}
TEST_CASE("Plugin audit a scoped root can also be registered read-only", "[audit]")
{
ScopedDataDir data_dir_guard("plugin-audit-scoped-readonly");
seed_denied_names();
seed_denied_keywords();
PluginAuditManager& mgr = PluginAuditManager::instance();
const fs::path readonly_dir = fs::path(data_dir()) / "readonly_scope";
fs::create_directories(readonly_dir);
ScopedPluginAuditContext ctx("test_plugin", "");
mgr.add_scoped_allowed_root(readonly_dir, /*allow_write=*/false);
SECTION("a read inside the scoped read-only root is allowed")
{
CHECK(mgr.check_open((readonly_dir / "vendor.json").string(), "r").allowed);
}
SECTION("a write inside the scoped read-only root is blocked")
{
CHECK_FALSE(mgr.check_open((readonly_dir / "vendor.json").string(), "w").allowed);
}
}

View File

@@ -112,22 +112,6 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
REQUIRE(read_install_state(plugin_dir, state)); REQUIRE(read_install_state(plugin_dir, state));
CHECK(state.installed_version == "1.2.0"); CHECK(state.installed_version == "1.2.0");
state.permissions.fs_read = {"/path/to/read"};
state.permissions.fs_readwrite = {"/path/to/readwrite"};
state.permissions.network_http = {"https://api.example.com"};
state.permissions.network_socket = {"192.168.45.6:443"};
state.permissions.process = {"/usr/bin/curl"};
REQUIRE(write_install_state(plugin_dir, state));
// Permission data is persisted in the same sidecar as the installation metadata.
PluginInstallState persisted;
REQUIRE(read_install_state(plugin_dir, persisted));
CHECK(persisted.permissions.fs_read == state.permissions.fs_read);
CHECK(persisted.permissions.fs_readwrite == state.permissions.fs_readwrite);
CHECK(persisted.permissions.network_http == state.permissions.network_http);
CHECK(persisted.permissions.network_socket == state.permissions.network_socket);
CHECK(persisted.permissions.process == state.permissions.process);
// Reading the sidecar back onto a freshly-scanned descriptor (whose header version is still // Reading the sidecar back onto a freshly-scanned descriptor (whose header version is still
// 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare // 1.0.0) must surface the cloud-installed 1.2.0. This is what lets update_cloud_metadata compare
// the cloud's latest version against the installed version instead of the stale header, so an // the cloud's latest version against the installed version instead of the stale header, so an
@@ -136,4 +120,4 @@ TEST_CASE("install-state sidecar is the source of truth for a cloud plugin's ins
scanned.version = "1.0.0"; // as parsed from the unchanged PEP723 header scanned.version = "1.0.0"; // as parsed from the unchanged PEP723 header
read_install_state(plugin_dir, scanned); read_install_state(plugin_dir, scanned);
CHECK(scanned.installed_version == "1.2.0"); CHECK(scanned.installed_version == "1.2.0");
} }