Merge branch 'main' into fix/opc-support-for-ota

This commit is contained in:
Ian Chua
2026-09-03 14:54:51 +08:00
97 changed files with 2755 additions and 2147 deletions
+2 -2
View File
@@ -149,9 +149,9 @@ jobs:
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
run: | run: |
if [ -z "${{ vars.SELF_HOSTED }}" ]; then if [ -z "${{ vars.SELF_HOSTED }}" ]; then
brew install automake texinfo libtool brew install automake texinfo libtool pkgconf yasm nasm
fi fi
./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15 ./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -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 {} +)
+2 -2
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 && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }} ./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -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 && '-1' || '' }} -a universal -t 10.15 ./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -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
+67 -1
View File
@@ -298,6 +298,8 @@ if (APPLE)
SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer") SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer")
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}") message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN")
endif () endif ()
# Proposal for C++ unit tests and sandboxes # Proposal for C++ unit tests and sandboxes
@@ -1065,6 +1067,10 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
${CMAKE_PREFIX_PATH}/bin/occt/TKXDESTEP.dll ${CMAKE_PREFIX_PATH}/bin/occt/TKXDESTEP.dll
${CMAKE_PREFIX_PATH}/bin/occt/TKXSBase.dll ${CMAKE_PREFIX_PATH}/bin/occt/TKXSBase.dll
${CMAKE_PREFIX_PATH}/bin/freetype.dll ${CMAKE_PREFIX_PATH}/bin/freetype.dll
${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll
${CMAKE_PREFIX_PATH}/bin/swresample-5.dll
${CMAKE_PREFIX_PATH}/bin/swscale-8.dll
${CMAKE_PREFIX_PATH}/bin/avutil-59.dll
DESTINATION ${_out_dir}) DESTINATION ${_out_dir})
set(${output_dlls} set(${output_dlls}
@@ -1100,12 +1106,58 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
${_out_dir}/TKXSBase.dll ${_out_dir}/TKXSBase.dll
${_out_dir}/freetype.dll ${_out_dir}/freetype.dll
${_out_dir}/avcodec-61.dll
${_out_dir}/swresample-5.dll
${_out_dir}/swscale-8.dll
${_out_dir}/avutil-59.dll
PARENT_SCOPE PARENT_SCOPE
) )
endfunction() endfunction()
function(orcaslicer_copy_sos target config postfix output_sos)
get_property(_is_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
get_target_property(_alt_out_dir ${target} RUNTIME_OUTPUT_DIRECTORY)
if (_alt_out_dir)
set(_out_dir "${_alt_out_dir}")
elseif (_is_multi)
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}/${config}")
else ()
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}")
endif ()
file(COPY ${CMAKE_PREFIX_PATH}/lib/libavcodec.so
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100
${CMAKE_PREFIX_PATH}/lib/libavutil.so
${CMAKE_PREFIX_PATH}/lib/libavutil.so.59
${CMAKE_PREFIX_PATH}/lib/libavutil.so.59.8.100
${CMAKE_PREFIX_PATH}/lib/libswscale.so
${CMAKE_PREFIX_PATH}/lib/libswscale.so.8
${CMAKE_PREFIX_PATH}/lib/libswscale.so.8.1.100
${CMAKE_PREFIX_PATH}/lib/libswresample.so
${CMAKE_PREFIX_PATH}/lib/libswresample.so.5
${CMAKE_PREFIX_PATH}/lib/libswresample.so.5.1.100
DESTINATION ${_out_dir})
set(${output_sos}
${_out_dir}/libavcodec.so
${_out_dir}/libavcodec.so.61
${_out_dir}/libavcodec.so.61.3.100
${_out_dir}/libavutil.so
${_out_dir}/libavutil.so.59
${_out_dir}/libavutil.so.59.8.100
${_out_dir}/libswscale.so
${_out_dir}/libswscale.so.8
${_out_dir}/libswscale.so.8.1.100
${_out_dir}/libswresample.so
${_out_dir}/libswresample.so.5
${_out_dir}/libswresample.so.5.1.100
PARENT_SCOPE
)
endfunction()
# Bundled sources set their own warning flags, and a plain -Wall there means /Wall # Bundled sources set their own warning flags, and a plain -Wall there means /Wall
# (= -Weverything) under clang-cl. Target options are applied after the ones a target # (= -Weverything) under clang-cl. Target options are applied after the ones a target
@@ -1217,6 +1269,20 @@ else ()
endif() endif()
endif () endif ()
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(LIBRARY_FILES
${LIBDIR_BIN}/libavcodec.so.61
${LIBDIR_BIN}/libavcodec.so.61.3.100
${LIBDIR_BIN}/libavutil.so.59
${LIBDIR_BIN}/libavutil.so.59.8.100
${LIBDIR_BIN}/libswresample.so.5
${LIBDIR_BIN}/libswresample.so.5.1.100
${LIBDIR_BIN}/libswscale.so.8
${LIBDIR_BIN}/libswscale.so.8.1.100
)
install(FILES ${LIBRARY_FILES} DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
endif ()
install(FILES ${CMAKE_SOURCE_DIR}/LICENSE.txt DESTINATION ".") install(FILES ${CMAKE_SOURCE_DIR}/LICENSE.txt DESTINATION ".")
configure_file(${LIBDIR}/dev-utils/platform/unix/fhs.hpp.in ${LIBDIR_BIN}/dev-utils/platform/unix/fhs.hpp) configure_file(${LIBDIR}/dev-utils/platform/unix/fhs.hpp.in ${LIBDIR_BIN}/dev-utils/platform/unix/fhs.hpp)
+4 -4
View File
@@ -4,7 +4,7 @@ set -e
set -o pipefail set -o pipefail
SECONDS=0 SECONDS=0
while getopts ":dpa:snt:xbc:i:1Tuh" opt; do while getopts ":dpa:snt:xbc:i:j:Tuh" 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:1Tuh" 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"
;; ;;
1 ) j )
export CMAKE_BUILD_PARALLEL_LEVEL=1 export CMAKE_BUILD_PARALLEL_LEVEL="$OPTARG"
;; ;;
T ) T )
export BUILD_TESTS="1" export BUILD_TESTS="1"
@@ -58,7 +58,7 @@ while getopts ":dpa:snt:xbc:i:1Tuh" 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 " -1: Use single job for building" echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
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
;; ;;
+2 -1
View File
@@ -375,7 +375,7 @@ include(libnoise/libnoise.cmake)
include(Draco/Draco.cmake) include(Draco/Draco.cmake)
# Assimp: glTF/GLB/FBX import for the texture-to-color feature. include(FFMPEG/FFMPEG.cmake)
include(Assimp/Assimp.cmake) include(Assimp/Assimp.cmake)
@@ -459,6 +459,7 @@ set(_dep_list
dep_libnoise dep_libnoise
dep_python3 dep_python3
dep_wxInspector dep_wxInspector
dep_FFMPEG
dep_Assimp dep_Assimp
) )
+87
View File
@@ -0,0 +1,87 @@
set(_conf_cmd ./configure)
if (MSVC)
set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG")
set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-winarm64-orca-shared-7.0.zip")
set(PREBUILD_HASH_arm64 "12f4140279f2f8469885e1b5b2e8be9d788882914c21523cacd56989f3548054")
set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-win64-orca-shared-7.0.zip")
set(PREBUILD_HASH_x64 "e65916020ddb9ef84b2666dfbcbfc9b1d67f69d15b4a66db53754637bf2d498c")
ExternalProject_Add(dep_FFMPEG
URL ${PREBUILD_URL_${DEPS_ARCH}}
URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}}
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/bin" "${DESTDIR}/bin"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/lib" "${DESTDIR}/lib"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${_source_dir}/include" "${DESTDIR}/include"
)
else ()
if (APPLE)
set(_minos_cmd
"--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
"--extra-ldflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
)
# Static FFmpeg: nothing to bundle into the .app, no rpath handling.
# Disable the VideoToolbox/AudioToolbox HW-accel paths: the player decodes
# in software (swscale), and the auto-detected HW objects would drag in
# system frameworks that the static libs would then depend on.
set(_link_cmd --enable-static --disable-shared --disable-videotoolbox --disable-audiotoolbox)
if (IS_CROSS_COMPILE)
set(_cross_cmd --enable-cross-compile)
set(_pic_cmd --enable-pic)
if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "x86_64")
set(_arch_cmd --arch=arm64)
set(_cc_cmd "--cc=clang -arch arm64")
else()
set(_arch_cmd --arch=x86_64)
set(_cc_cmd "--cc=clang -arch x86_64")
endif()
endif()
else ()
set(_link_cmd --enable-shared)
endif ()
set(_build_j -j)
if(DEFINED ENV{CMAKE_BUILD_PARALLEL_LEVEL})
set(_build_j "-j$ENV{CMAKE_BUILD_PARALLEL_LEVEL}")
endif()
ExternalProject_Add(dep_FFMPEG
URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz
URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
CONFIGURE_COMMAND ${_conf_cmd}
${_cross_cmd}
${_pic_cmd}
${_arch_cmd}
${_cc_cmd}
"--prefix=${DESTDIR}"
${_link_cmd}
${_minos_cmd}
--disable-doc
--enable-small
--disable-outdevs
--disable-filters
--enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test*
--disable-protocols
--enable-protocol=file,fd,pipe,rtp,udp
--disable-muxers
--enable-muxer=rtp
--disable-encoders
--disable-decoders
--enable-decoder=*aac*,h264*,mp3*,mjpeg,rv*
--disable-demuxers
--enable-demuxer=h264,mp3,mov
--disable-zlib
--disable-avdevice
BUILD_IN_SOURCE ON
BUILD_COMMAND make ${_build_j}
INSTALL_COMMAND make install
)
endif()
@@ -0,0 +1,376 @@
# macOS FFmpeg Media Player Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make macOS use the same FFmpeg-based media player (`wxMediaCtrl3` + `AVVideoDecoder`) as Windows/Linux, linking the static FFmpeg libraries from the deps build, and remove the old `wxMediaCtrl2.mm` BambuPlayer-based player.
**Architecture:** The new player is platform-neutral C++ already used on Linux/Windows. Enabling it on macOS is pure build wiring: compile `wxMediaCtrl3.cpp` + `AVVideoDecoder.cpp` on macOS, drop the `__WXMAC__` alias that redirects `wxMediaCtrl3` to the old `wxMediaCtrl2`, and link static FFmpeg (`libavcodec.a`/`libswscale.a`/`libavutil.a`) from the deps install. The Bambu stream API is dlsym'd at runtime from the network plugin (`libBambuSource.dylib`), which already exports it — no plugin changes needed. Rendering reuses the existing `wxImage` → `DrawBitmap` paint path (same as Linux).
**Tech Stack:** C++17, wxWidgets, CMake, FFmpeg 7.0.3 (libavcodec/libswscale/libavutil), macOS (Xcode generator), `deps/` ExternalProject build system.
## Global Constraints
- Branch: `dev/ffmpeg-player-macos`. Commit after every task.
- **Linux and Windows builds must not change** — the FFmpeg deps flag change is guarded by `APPLE`; Linux keeps `--enable-shared`, Windows keeps its prebuilt DLL zips.
- Static FFmpeg only on macOS: deps produce `libavcodec.a`/`libswscale.a`/`libavutil.a`; the app links those explicitly — the app binary must have **no** `libav*` dylib references (`otool -L` check).
- Follow existing code style: PascalCase classes, snake_case functions, C++17.
- No changes to `StatusPanel.cpp`, `MediaPlayCtrl.*`, or the BambuTunnel interface — the app already creates `wxMediaCtrl3` and uses only its public interface.
- The player cannot be unit-tested (hardware/plugin-dependent GUI code); verification is build-level, link-level, and manual runtime on a Mac.
- `localization/i18n/list.txt` references only `wxMediaCtrl2.cpp` (Win/Linux, stays) — no translation-list changes needed.
- Build dirs on the dev machine: main app = `build_arm64/` (Xcode generator, multi-config), deps = `deps/build/arm64/` (Unix Makefiles). App target name: `OrcaSlicer`. Substitute your own configured build dirs where noted.
---
### Task 1: Enable wxMediaCtrl3 on macOS and link static FFmpeg
**Files:**
- Modify: `src/slic3r/GUI/wxMediaCtrl3.h` (lines 18–22: the `#ifdef __WXMAC__` alias branch)
- Modify: `src/slic3r/GUI/wxMediaCtrl3.cpp:13` (uncomment the event define)
- Modify: `src/slic3r/GUI/wxMediaCtrl2.cpp:101` (remove the event define)
- Modify: `src/slic3r/CMakeLists.txt` (APPLE source list ~lines 779–792; FFmpeg link block ~lines 905–910)
**Interfaces:**
- Consumes: nothing new (all classes already exist).
- Produces: `wxMediaCtrl3` class compiled on macOS with the same interface as Linux/Windows — `Load(wxURI)`, `Play()`, `Stop()`, `SetIdleImage(wxString)`, `GetState()`, `GetLastError()`, `GetVideoSize()`, event `EVT_MEDIA_CTRL_STAT` defined once in the lib (from `wxMediaCtrl3.cpp`).
- [ ] **Step 1: Remove the macOS alias in wxMediaCtrl3.h**
Current (lines 16–23 of `src/slic3r/GUI/wxMediaCtrl3.h`):
```cpp
void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height);
#ifdef __WXMAC__
#include "wxMediaCtrl2.h"
#define wxMediaCtrl3 wxMediaCtrl2
#else
#define BAMBU_DYNAMIC
```
New:
```cpp
void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height);
#define BAMBU_DYNAMIC
```
Also remove the matching `#endif` that closed the `#else` branch (the one before the final `#endif /* wxMediaCtrl3_h */`), so the file's `#ifndef`/`#endif` guard pair stays balanced.
- [ ] **Step 2: Move the EVT_MEDIA_CTRL_STAT definition into wxMediaCtrl3.cpp**
In `src/slic3r/GUI/wxMediaCtrl3.cpp:13`, uncomment:
```cpp
//wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
```
becomes:
```cpp
wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
```
In `src/slic3r/GUI/wxMediaCtrl2.cpp:101`, delete:
```cpp
wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
```
(One definition total in the lib — `MediaPlayCtrl.cpp:59` binds this event on the media ctrl.)
- [ ] **Step 3: Update the APPLE source list in CMakeLists.txt**
In `src/slic3r/CMakeLists.txt`, the APPLE branch (currently compiles `wxMediaCtrl2.mm`, which becomes dead on macOS):
```cmake
GUI/wxMediaCtrl2.mm
GUI/wxMediaCtrl2.h
GUI/wxMediaCtrl3.h
)
```
becomes:
```cmake
GUI/AVVideoDecoder.cpp
GUI/AVVideoDecoder.hpp
GUI/wxMediaCtrl3.cpp
GUI/wxMediaCtrl3.h
)
```
(The `else ()` branch — Win/Linux — stays exactly as it is.)
- [ ] **Step 4: Link static FFmpeg on macOS**
In `src/slic3r/CMakeLists.txt`, the FFmpeg block (currently `if (NOT APPLE)`):
```cmake
if (NOT APPLE)
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavcodec
libswscale
libavutil
)
target_link_libraries(libslic3r_gui PkgConfig::LIBAV)
endif()
```
becomes:
```cmake
if (APPLE)
# Static FFmpeg from the deps install: nothing to bundle into the .app,
# no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil.
find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
else ()
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavcodec
libswscale
libavutil
)
target_link_libraries(libslic3r_gui PkgConfig::LIBAV)
endif()
```
The deps install (`${CMAKE_PREFIX_PATH}/lib`) already contains the three `.a` files from the existing arm64 deps build — no deps rebuild needed for this task.
- [ ] **Step 5: Reconfigure and build the app**
Run (Xcode generator; `cmake` re-runs automatically on build):
```bash
cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer
```
Expected: configure succeeds (no `pkg_check_modules` errors on macOS, `find_library` finds all three `.a` files), compile succeeds (`wxMediaCtrl3.cpp` and `AVVideoDecoder.cpp` compile on macOS without changes), link succeeds.
If CMake complains that `wxMediaCtrl3.h` is included but not in the source list or similar IDE-only warnings — ignore; headers in the list are cosmetic.
- [ ] **Step 6: Verify no dynamic FFmpeg dependency**
```bash
otool -L build_arm64/src/RelWithDebInfo/OrcaSlicer.app/Contents/MacOS/OrcaSlicer | grep -i "libav" || echo "OK: no dynamic FFmpeg"
```
Expected: prints `OK: no dynamic FFmpeg` (empty grep output). This is the whole point of static linking — nothing to bundle into the `.app`.
- [ ] **Step 7: Quick sanity — macOS unit tests still pass**
```bash
ctest --test-dir build_arm64/tests/libslic3r --output-on-failure
```
Expected: passes (add `-C RelWithDebInfo` if the multi-config generator requires it). If no tests were built in this build dir, build target `tests` first (`cmake --build build_arm64 --config RelWithDebInfo --target tests`).
- [ ] **Step 8: Commit**
```bash
git add src/slic3r/CMakeLists.txt src/slic3r/GUI/wxMediaCtrl3.h src/slic3r/GUI/wxMediaCtrl3.cpp src/slic3r/GUI/wxMediaCtrl2.cpp
git commit -m "feat: use FFmpeg media player on macOS with static FFmpeg"
```
---
### Task 2: Static-only FFmpeg in the macOS deps build
**Files:**
- Modify: `deps/FFMPEG/FFMPEG.cmake` (non-MSVC branch, APPLE section and CONFIGURE_COMMAND)
**Interfaces:**
- Consumes: nothing.
- Produces: a deps install on macOS containing only `libavcodec.a`, `libswscale.a`, `libavutil.a` (+ headers) — no `libav*` dylibs, so no bundling/rpath machinery is ever needed on macOS. Linux and Windows output are unchanged.
- [ ] **Step 1: Add the static flag variable**
In `deps/FFMPEG/FFMPEG.cmake`, inside the non-MSVC `else ()` branch, in the existing `if (APPLE)` block:
```cmake
if (APPLE)
set(_minos_cmd
"CFLAGS=-mmacosx-version-min=${DEP_OSX_TARGET}"
"LDFLAGS=-mmacosx-version-min=${DEP_OSX_TARGET}"
)
```
add after the `_minos_cmd` set:
```cmake
# Static FFmpeg: nothing to bundle into the .app, no rpath handling.
# Shared flags must come AFTER --enable-shared below so they win.
set(_link_cmd --enable-static --disable-shared)
```
and add a matching `else ()` after the `if (IS_CROSS_COMPILE) ... endif()` block inside that `if (APPLE)`, so non-Apple Unix keeps shared:
```cmake
else ()
set(_link_cmd --enable-shared)
endif ()
```
(If the existing `if (IS_CROSS_COMPILE)` block is the last thing inside `if (APPLE)`, the new `else ()` closes the `if (APPLE)` itself.)
- [ ] **Step 2: Use the variable in CONFIGURE_COMMAND**
In the `ExternalProject_Add(dep_FFMPEG ...)` configure command:
```cmake
"--prefix=${DESTDIR}"
--enable-shared
```
becomes:
```cmake
"--prefix=${DESTDIR}"
--enable-shared
${_link_cmd}
```
Order matters: `--enable-shared` comes first, then `--enable-static --disable-shared` (APPLE) or `--enable-shared` (Linux) — the last flag wins in FFmpeg configure.
- [ ] **Step 3: Rebuild the FFmpeg dep (slow — several minutes, run in background)**
The changed CONFIGURE_COMMAND invalidates the ExternalProject stamp, so this re-configures and rebuilds FFmpeg:
```bash
cmake --build deps/build/arm64 --target dep_FFMPEG
```
For a fully clean static-only check (removes the previous shared build tree, which can leave stale `.dylib` files behind in the in-source build):
```bash
rm -rf deps/build/arm64/dep_FFMPEG-prefix
cmake --build deps/build/arm64 --target dep_FFMPEG
```
- [ ] **Step 4: Verify the artifacts**
```bash
ls deps/build/arm64/dep_FFMPEG-prefix/src/dep_FFMPEG/libavcodec/*.a
ls deps/build/arm64/dep_FFMPEG-prefix/src/dep_FFMPEG/libavcodec/*.dylib 2>/dev/null || echo "OK: no dylibs"
```
Expected: `libavcodec.a` present, second command prints `OK: no dylibs`. Check `libavutil` and `libswscale` the same way.
- [ ] **Step 5: Verify the app still links against the static libs**
```bash
cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer
otool -L build_arm64/src/RelWithDebInfo/OrcaSlicer.app/Contents/MacOS/OrcaSlicer | grep -i "libav" || echo "OK: no dynamic FFmpeg"
```
Expected: build succeeds, `OK: no dynamic FFmpeg`.
- [ ] **Step 6: Commit**
```bash
git add deps/FFMPEG/FFMPEG.cmake
git commit -m "build: build static-only FFmpeg for macOS deps"
```
---
### Task 3: Remove the old macOS player
**Files:**
- Delete: `src/slic3r/GUI/wxMediaCtrl2.mm`
- Delete: `src/slic3r/GUI/BambuPlayer/BambuPlayer.h` (and the empty `BambuPlayer/` dir)
- Modify: `src/slic3r/GUI/wxMediaCtrl2.h` (remove the `#ifdef __WXMAC__` section, lines 22–60)
**Interfaces:**
- Consumes: Task 1 (macOS no longer references `wxMediaCtrl2` — nothing includes `wxMediaCtrl2.h` on macOS anymore; `wxMediaCtrl2` is never instantiated on any platform).
- Produces: a clean tree where the old BambuPlayer-based player is gone from macOS. The `BambuPlayer` ObjC class itself remains inside the network plugin (external prebuilt binary) — only the GUI-side consumer is removed.
- [ ] **Step 1: Delete the old player files**
```bash
git rm src/slic3r/GUI/wxMediaCtrl2.mm
git rm src/slic3r/GUI/BambuPlayer/BambuPlayer.h
rmdir src/slic3r/GUI/BambuPlayer 2>/dev/null || true
```
- [ ] **Step 2: Strip the __WXMAC__ section from wxMediaCtrl2.h**
In `src/slic3r/GUI/wxMediaCtrl2.h`, remove the entire macOS branch of the `#ifdef __WXMAC__` guard — from `#ifdef __WXMAC__` (line 22) through the closing `};` of the mac class (line 60), and the `#else` marker — leaving only the non-mac `class wxMediaCtrl2 : public wxMediaCtrl { ... };` definition followed by the final `#endif /* wxMediaCtrl2_h */`. The resulting file keeps its `#ifndef`/`#endif` include guard pair balanced.
The file stays on disk because Win/Linux compile `wxMediaCtrl2.cpp`, which includes it.
- [ ] **Step 3: Grep for leftover references**
```bash
grep -rn "wxMediaCtrl2.mm\|BambuPlayer/BambuPlayer.h\|BambuPlayer" src/slic3r --include="*.cpp" --include="*.h" --include="*.mm" --include="*.txt"
```
Expected: no hits in `src/slic3r/GUI` (ignore `localization/i18n/list.txt:196`, which lists the Win/Linux `wxMediaCtrl2.cpp` and stays).
- [ ] **Step 4: Rebuild the app**
```bash
cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer
```
Expected: configure + compile + link succeed with the deleted files gone.
- [ ] **Step 5: Commit**
```bash
git add -A src/slic3r/GUI
git commit -m "refactor: remove old BambuPlayer-based media player from macOS"
```
---
### Task 4: Runtime verification on hardware
**Files:** none — manual verification.
**Interfaces:** consumes all prior tasks. Final gate: the new player must actually stream on a Mac.
- [ ] **Step 1: Launch the freshly built app**
```bash
open build_arm64/src/RelWithDebInfo/OrcaSlicer.app
```
Expected: app launches normally; no crash in the network/device subsystem.
- [ ] **Step 2: Load the network plugin and open the Device tab**
Log in / ensure the network plugin (`libBambuSource.dylib`) loads, select a printer, open the Device tab (camera monitoring panel).
Expected: the camera preview area shows the idle image initially (no crash — this exercises `wxMediaCtrl3::SetIdleImage` and the `wxImage` load path on macOS for the first time).
- [ ] **Step 3: Start the stream and watch it render**
Click play / wait for `MediaPlayCtrl` to start the stream.
Expected: live video renders in the panel. Check the console/log output (`BOOST_LOG` goes to the terminal if run from it, or check the log file):
- `stat_log ...` lines appear (the `EVT_MEDIA_CTRL_STAT` path is live — proves the Bambu C API dlsym worked from `libBambuSource.dylib`);
- no repeated decode/error messages like `AVVideoDecoder: ...` or `can not find function ...` (proves `StaticBambuLib::get` resolved all Bambu functions);
- Stop/Play toggle works; idle image reappears on stop;
- window resize keeps aspect ratio (exercises `DoSetSize`/`adjust_frame_size`/`paintEvent`).
- [ ] **Step 4: Confirm the old player is really gone**
Expected: nothing in the logs references `BambuPlayer` (the ObjC class is no longer dlsym'd); the video path is entirely `wxMediaCtrl3` + `AVVideoDecoder`.
If a printer is unavailable, at minimum verify Steps 1–2 (launch + idle image) and note in the PR that live-stream verification needs hardware.
- [ ] **Step 5: Final review pass**
```bash
git log --oneline -6
git show --stat HEAD # and each of the three task commits
```
Expected: the last 4 commits are the design doc + the 3 implementation tasks (each task commit touches only its listed files). Review the diff for scope: no Linux/Windows changes beyond the two `EVT_MEDIA_CTRL_STAT` lines in Task 1, no `StatusPanel`/`MediaPlayCtrl` changes.
@@ -0,0 +1,110 @@
# FFmpeg Media Player for macOS — Design
Date: 2026-08-14
Branch: `dev/ffmpeg-player-macos`
## Problem
The branch's new FFmpeg-based media player (`wxMediaCtrl3` + `AVVideoDecoder`) is used on
Windows and Linux, but macOS still runs the old player: `wxMediaCtrl2.mm`, an ObjC
`BambuPlayer` class dlsym'd from the Bambu network plugin that renders via CALayer.
On macOS, `wxMediaCtrl3` is currently aliased to `wxMediaCtrl2` and FFmpeg is not linked
into the app at all.
Goal: make macOS use the same FFmpeg player as Windows/Linux, linking the **static**
FFmpeg libraries from the deps build instead of dynamic ones.
## Current state (verified)
- New player (Win/Linux): `GUI/wxMediaCtrl3.cpp` + `GUI/AVVideoDecoder.cpp`. Decodes with
FFmpeg (libavcodec/libswscale/libavutil), renders frames into `wxImage` (non-Windows) /
`wxBitmap` (Windows) drawn in a `paintEvent`, feeds via the `Bambu_*` C API
(`BambuTunnel.h`, `BAMBU_DYNAMIC`) dlsym'd from the network plugin through
`StaticBambuLib::get()` (`GUI/Printer/PrinterFileSystem.cpp`, compiled on all platforms).
- Old player (macOS): `GUI/wxMediaCtrl2.mm` uses the ObjC `BambuPlayer` class found via
`dlsym(module, "OBJC_CLASS_$_BambuPlayer")` in `libBambuSource.dylib`.
- The macOS network plugin `libBambuSource.dylib` already exports the full Bambu C API
(verified with `nm`), so the new player needs zero plugin changes.
- FFmpeg linking in `src/slic3r/CMakeLists.txt` is guarded by `if (NOT APPLE)` —
macOS currently does not link FFmpeg.
- `deps/FFMPEG/FFMPEG.cmake`: non-MSVC branch builds FFmpeg from source with
`--enable-shared`. The existing arm64 deps build on the dev machine happened to be
configured with both static and shared enabled, so `libavcodec.a` / `libswscale.a` /
`libavutil.a` are already present at
`deps/build/arm64/OrcaSlicer_dep/usr/local/lib/`.
- `EVT_MEDIA_CTRL_STAT` is `wxDEFINE_EVENT`'d in `wxMediaCtrl2.cpp` (Win/Linux) and
`wxMediaCtrl2.mm` (macOS); the define in `wxMediaCtrl3.cpp` is commented out.
- `wxMediaCtrl2` is never instantiated anywhere on any platform — dead code.
- `StatusPanel` already creates `wxMediaCtrl3`; `MediaPlayCtrl` only uses the
`wxMediaCtrl3` interface (`Load/Play/Stop/GetState/GetVideoSize/GetLastError/SetIdleImage`),
so no UI-side changes are needed.
## Approach (approved)
**Reuse the shared player on macOS.** Compile the existing `wxMediaCtrl3.cpp` +
`AVVideoDecoder.cpp` on macOS so all three platforms run one implementation.
Rendering uses the existing `wxImage` → `DrawBitmap` paint path, identical to Linux.
Known trade-off: frames are scaled to the widget's logical (1x) size, so Retina is
slightly soft compared to the old CALayer player. Accepted for now; a Retina-aware
scaling follow-up is possible later.
Rejected alternative: a native CGImage/CALayer renderer for macOS — faster and
Retina-crisp, but adds a second render implementation to maintain.
## Changes
### 1. Enable the FFmpeg player on macOS (source)
- `GUI/wxMediaCtrl3.h`: remove the `#ifdef __WXMAC__` branch (lines 18–22) that aliases
`wxMediaCtrl3` → `wxMediaCtrl2`. macOS then compiles the real `wxMediaCtrl3` class,
including the `BAMBU_DYNAMIC` BambuTunnel path used on Linux.
- Event symbol fix: move `wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent)` into
`wxMediaCtrl3.cpp` (uncomment the existing line) and remove it from
`wxMediaCtrl2.cpp`. One definition total in the lib; all three platforms resolve it.
### 2. Static FFmpeg linking (deps + app)
- `deps/FFMPEG/FFMPEG.cmake`: in the non-MSVC branch, pass
`--disable-shared --enable-static` when `APPLE`. Linux keeps `--enable-shared`;
Windows keeps its prebuilt shared DLL zips. Fresh macOS deps builds install only
`libavcodec.a` / `libswscale.a` / `libavutil.a` — no dylibs to bundle, no
rpath/install_name handling. (The existing local arm64 deps build already contains
the `.a` files, so no deps rebuild is strictly needed to try the change locally,
but a fresh CI deps build must produce them.)
- `src/slic3r/CMakeLists.txt`:
- APPLE branch of `SLIC3R_GUI_SOURCES`: add `GUI/wxMediaCtrl3.cpp`,
`GUI/wxMediaCtrl3.h`, `GUI/AVVideoDecoder.cpp`, `GUI/AVVideoDecoder.hpp`;
remove `GUI/wxMediaCtrl2.mm` and `GUI/wxMediaCtrl2.h` (the `.h` stays on
disk for the Win/Linux build of `wxMediaCtrl2.cpp`, but nothing on macOS
includes it after this change).
- Add an APPLE mirror of the `NOT APPLE` FFmpeg block: `find_library` for
`libavcodec.a`, `libswscale.a`, `libavutil.a` under `${CMAKE_PREFIX_PATH}/lib`
with `NO_DEFAULT_PATH`, link them (order avcodec → swscale → avutil), and add
`${CMAKE_PREFIX_PATH}/include` as a SYSTEM include directory. Deps are built with
`--disable-zlib` and no external codecs, so the three static libs link cleanly.
### 3. Remove the old player
- Delete `GUI/wxMediaCtrl2.mm` and `GUI/BambuPlayer/BambuPlayer.h` (header used only
by the `.mm`; the real `BambuPlayer` lives inside the network plugin).
- Remove the now-dead `__WXMAC__` section of `GUI/wxMediaCtrl2.h`.
- `wxMediaCtrl2.cpp` (Win/Linux) stays in the build as-is (dead but harmless; out of
scope to remove on this branch).
### 4. Verification
- Build on macOS: `cmake --build build_arm64` (or `build/arm64`).
- Confirm no dynamic FFmpeg dependency: `otool -L` on the app binary shows no `libav*`
dylib references.
- Runtime: with the network plugin loaded, the Device tab camera preview streams via
the FFmpeg player (check the device page / `MediaPlayCtrl`).
- macOS `ctest` still passes — static linking means no test-executable `.so` copying
hacks (unlike the Linux shared-lib setup).
## Out of scope
- Linux (shared libs, AppImage/flatpak bundling) and Windows (prebuilt DLL zips)
keep their current FFmpeg setup.
- Retina-aware frame scaling / native CGImage rendering (follow-up if visual quality
is judged insufficient).
- Audio streaming (neither player plays audio in this UI path).
-1
View File
@@ -193,7 +193,6 @@ src/slic3r/GUI/ObjColorDialog.cpp
src/slic3r/GUI/SyncAmsInfoDialog.cpp src/slic3r/GUI/SyncAmsInfoDialog.cpp
src/slic3r/GUI/WipeTowerDialog.cpp src/slic3r/GUI/WipeTowerDialog.cpp
src/slic3r/GUI/wxExtensions.cpp src/slic3r/GUI/wxExtensions.cpp
src/slic3r/GUI/wxMediaCtrl2.cpp
src/slic3r/GUI/WebUserLoginDialog.cpp src/slic3r/GUI/WebUserLoginDialog.cpp
src/slic3r/GUI/WebGuideDialog.cpp src/slic3r/GUI/WebGuideDialog.cpp
src/slic3r/GUI/KBShortcutsDialog.hpp src/slic3r/GUI/KBShortcutsDialog.hpp
Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 579 KiB

@@ -318,6 +318,12 @@ modules:
sha256: 0ba163956f2d468b19a91b96c5aba66ee9610843ea41dda628ea44cdafde7db7 sha256: 0ba163956f2d468b19a91b96c5aba66ee9610843ea41dda628ea44cdafde7db7
dest: external-packages/wxInspector dest: external-packages/wxInspector
# FFmpeg n7.0.3
- type: file
url: https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz
sha256: deedcabe339165214a3637df4c86a507aef0d793cf8774ff68735f4737e8ddbc
dest: external-packages/FFMPEG
# --------------------------------------------------------------- # ---------------------------------------------------------------
# Fallback archives for deps normally provided by the GNOME SDK. # Fallback archives for deps normally provided by the GNOME SDK.
# These are only used if find_package() fails to locate them. # These are only used if find_package() fails to locate them.
+3
View File
@@ -25,6 +25,9 @@ export REQUIRED_DEV_PACKAGES=(
wayland-protocols wayland-protocols
webkit2gtk-4.1 webkit2gtk-4.1
wget wget
pkgconf
yasm
nasm
) )
if [[ -n "$UPDATE_LIB" ]] if [[ -n "$UPDATE_LIB" ]]
+3
View File
@@ -25,6 +25,9 @@ export REQUIRED_DEV_PACKAGES=(
wayland-protocols wayland-protocols
webkit2gtk webkit2gtk
wget wget
pkgconf
yasm
nasm
) )
if [[ -n "$UPDATE_LIB" ]] if [[ -n "$UPDATE_LIB" ]]
+1
View File
@@ -20,6 +20,7 @@ export REQUIRED_BUNDLES=(
perl-basic perl-basic
texinfo texinfo
wget wget
nasm
) )
if [[ -n "$UPDATE_LIB" ]] if [[ -n "$UPDATE_LIB" ]]
+3
View File
@@ -27,6 +27,9 @@ REQUIRED_DEV_PACKAGES=(
ninja-build ninja-build
texinfo texinfo
wget wget
pkgconf
yasm
nasm
) )
if [[ -n "$UPDATE_LIB" ]] if [[ -n "$UPDATE_LIB" ]]
+3
View File
@@ -31,6 +31,9 @@ REQUIRED_DEV_PACKAGES=(
webkit2gtk4.1-devel webkit2gtk4.1-devel
wget wget
libcurl-devel libcurl-devel
pkgconf
yasm
nasm
) )
if [[ -n "$UPDATE_LIB" ]] if [[ -n "$UPDATE_LIB" ]]
+3
View File
@@ -32,6 +32,9 @@ REQUIRED_DEV_PACKAGES=(
sys-devel/m4 sys-devel/m4
virtual/libudev virtual/libudev
x11-libs/gtk+:3 x11-libs/gtk+:3
dev-util/pkgconf
dev-lang/yasm
dev-lang/nasm
) )
if [[ -n "$UPDATE_LIB" ]] if [[ -n "$UPDATE_LIB" ]]
+3
View File
@@ -30,6 +30,9 @@ REQUIRED_DEV_PACKAGES=(
webkit2gtk4-devel webkit2gtk4-devel
wget wget
libcurl-devel libcurl-devel
pkgconf
yasm
nasm
) )
if [[ -n "$UPDATE_LIB" ]] if [[ -n "$UPDATE_LIB" ]]
+14
View File
@@ -294,6 +294,16 @@ if (WIN32)
endif() endif()
else () else ()
if (NOT APPLE)
set(output_sos_Release "")
set(output_sos_Debug "")
add_custom_target(OrcaSlicerSosCopy ALL DEPENDS OrcaSlicer)
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
orcaslicer_copy_sos(OrcaSlicerSosCopy "Debug" "d" output_sos_Debug)
else()
orcaslicer_copy_sos(OrcaSlicerSosCopy "Release" "" output_sos_Release)
endif()
endif()
if (APPLE AND NOT CMAKE_MACOSX_BUNDLE) if (APPLE AND NOT CMAKE_MACOSX_BUNDLE)
# On OSX, the name of the binary matches the name of the Application. # On OSX, the name of the binary matches the name of the Application.
add_custom_command(TARGET OrcaSlicer POST_BUILD add_custom_command(TARGET OrcaSlicer POST_BUILD
@@ -372,5 +382,9 @@ if (WIN32)
install(FILES ${output_dlls_${build_type}} DESTINATION ".") install(FILES ${output_dlls_${build_type}} DESTINATION ".")
install(DIRECTORY "${CMAKE_PREFIX_PATH}/libpython/" DESTINATION "python") install(DIRECTORY "${CMAKE_PREFIX_PATH}/libpython/" DESTINATION "python")
else () else ()
if (APPLE)
else()
install(FILES ${output_sos_${build_type}} DESTINATION "${CMAKE_INSTALL_PREFIX}")
endif()
install(TARGETS OrcaSlicer RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR}) install(TARGETS OrcaSlicer RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR})
endif () endif ()
@@ -83,6 +83,10 @@ copy_shared_object_to_dir() {
src_real="$(readlink -f "$src")" src_real="$(readlink -f "$src")"
dst_name="$(basename "$src_real")" dst_name="$(basename "$src_real")"
mkdir -p "$dst_dir" mkdir -p "$dst_dir"
if [ "$src_real" = "$dst_dir/$dst_name" ]; then
# Already bundled; the dependency resolved from the bundle directory.
return 0
fi
cp -fL "$src_real" "$dst_dir/$dst_name" cp -fL "$src_real" "$dst_dir/$dst_name"
if [ -L "$src" ]; then if [ -L "$src" ]; then
@@ -96,12 +100,23 @@ copy_shared_object_to_dir() {
} }
bundle_dependency_closure() { bundle_dependency_closure() {
local dst_dir="$1" local dst_dir
dst_dir="$(cd -- "$1" && pwd)"
shift shift
local -a queue=("$@") local -a queue=("$@")
local target dep dep_real copied_path local target dep dep_real dep_key copied_path
declare -A seen=() declare -A seen=()
# Dependencies are resolved with ldd, which only searches the default
# loader path. Deps-built shared libraries (e.g. the FFmpeg stack) are not
# installed there and carry no RUNPATH of their own, so once copied into
# the bundle ldd can no longer resolve one sibling from another
# (libavcodec -> libavutil) and reports it as missing. Extend the loader
# path with the bundle directory plus the source directories of files
# already bundled, so every library that was resolved once keeps resolving
# for its own dependencies. The audit script does the same
# (scripts/check_appimage_libs.sh).
local -a search_dirs=("$dst_dir")
while [ ${#queue[@]} -gt 0 ]; do while [ ${#queue[@]} -gt 0 ]; do
target="${queue[0]}" target="${queue[0]}"
@@ -122,17 +137,24 @@ bundle_dependency_closure() {
continue continue
fi fi
if [ -n "${seen[$dep_real]}" ]; then # Key dedup on the bundled file rather than the source path: once
# ldd resolves a library from the bundle directory (via the
# LD_LIBRARY_PATH above) its path is a dst_dir path, which differs
# from the source path the first resolution returned. Keying on
# the source path would re-copy the file onto itself.
dep_key="$dst_dir/$(basename "$dep_real")"
if [ -n "${seen[$dep_key]}" ]; then
continue continue
fi fi
seen[$dep_real]=1 seen[$dep_key]=1
copy_shared_object_to_dir "$dep" "$dst_dir" copy_shared_object_to_dir "$dep" "$dst_dir"
search_dirs+=("$(dirname "$dep_real")")
copied_path="$dst_dir/$(basename "$dep_real")" copied_path="$dst_dir/$(basename "$dep_real")"
if [ -e "$copied_path" ]; then if [ -e "$copied_path" ]; then
queue+=("$copied_path") queue+=("$copied_path")
fi fi
done < <(appimage_list_direct_dependencies "$target") done < <(LD_LIBRARY_PATH="$(IFS=:; printf '%s' "${search_dirs[*]}")${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" appimage_list_direct_dependencies "$target")
done done
} }
+1 -1
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; float first_layer_time = 0.0f;
// 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;
-1
View File
@@ -165,7 +165,6 @@ 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;
+36 -18
View File
@@ -38,6 +38,8 @@ set(SLIC3R_GUI_SOURCES
GUI/AuxiliaryDataViewModel.hpp GUI/AuxiliaryDataViewModel.hpp
GUI/AuxiliaryDialog.cpp GUI/AuxiliaryDialog.cpp
GUI/AuxiliaryDialog.hpp GUI/AuxiliaryDialog.hpp
GUI/AVVideoDecoder.cpp
GUI/AVVideoDecoder.hpp
GUI/Auxiliary.hpp GUI/Auxiliary.hpp
GUI/BackgroundSlicingProcess.cpp GUI/BackgroundSlicingProcess.cpp
GUI/BackgroundSlicingProcess.hpp GUI/BackgroundSlicingProcess.hpp
@@ -611,6 +613,8 @@ set(SLIC3R_GUI_SOURCES
GUI/WipeTowerDialog.cpp GUI/WipeTowerDialog.cpp
GUI/wxExtensions.cpp GUI/wxExtensions.cpp
GUI/wxExtensions.hpp GUI/wxExtensions.hpp
GUI/wxMediaCtrl3.cpp
GUI/wxMediaCtrl3.h
plugin/PythonInterpreter.cpp plugin/PythonInterpreter.cpp
plugin/PythonInterpreter.hpp plugin/PythonInterpreter.hpp
plugin/PythonPluginBridge.cpp plugin/PythonPluginBridge.cpp
@@ -799,21 +803,8 @@ if (APPLE)
GUI/DeepLinkHandlerMac.mm GUI/DeepLinkHandlerMac.mm
GUI/DeepLinkHandlerMac.h GUI/DeepLinkHandlerMac.h
GUI/GUI_UtilsMac.mm GUI/GUI_UtilsMac.mm
GUI/wxMediaCtrl2.mm
GUI/wxMediaCtrl2.h
) )
FIND_LIBRARY(DISKARBITRATION_LIBRARY DiskArbitration) FIND_LIBRARY(DISKARBITRATION_LIBRARY DiskArbitration)
else ()
list(APPEND SLIC3R_GUI_SOURCES
GUI/wxMediaCtrl2.cpp
GUI/wxMediaCtrl2.h
)
endif ()
if (UNIX AND NOT APPLE)
list(APPEND SLIC3R_GUI_SOURCES
GUI/Printer/gstbambusrc.c
)
endif () endif ()
set(ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY}") set(ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY}")
@@ -913,6 +904,38 @@ if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY)
add_precompiled_header(libslic3r_gui pchheader.hpp FORCEINCLUDE) add_precompiled_header(libslic3r_gui pchheader.hpp FORCEINCLUDE)
endif () endif ()
if (APPLE)
# Static FFmpeg from the deps install: nothing to bundle into the .app,
# no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil.
find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
message(FATAL_ERROR "Static FFmpeg (libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.")
endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
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 ()
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavcodec
libswscale
libavutil
)
target_link_libraries(libslic3r_gui PkgConfig::LIBAV)
endif()
# We need to implement some hacks for wxWidgets and touch the underlying GTK # We need to implement some hacks for wxWidgets and touch the underlying GTK
# layer and sub-libraries. This forces us to use the include locations and # layer and sub-libraries. This forces us to use the include locations and
# link these libraries. # link these libraries.
@@ -939,11 +962,6 @@ if (UNIX AND NOT APPLE)
target_compile_definitions(libslic3r_gui PRIVATE wxHAVE_GDK_WAYLAND) target_compile_definitions(libslic3r_gui PRIVATE wxHAVE_GDK_WAYLAND)
endif () endif ()
# We add GStreamer for bambu:/// support.
pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0)
pkg_check_modules(GST_BASE REQUIRED gstreamer-base-1.0)
target_link_libraries(libslic3r_gui ${GSTREAMER_LIBRARIES} ${GST_BASE_LIBRARIES})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GSTREAMER_INCLUDE_DIRS} ${GST_BASE_INCLUDE_DIRS})
endif () endif ()
# Add a definition so that we can tell we are compiling slic3r. # Add a definition so that we can tell we are compiling slic3r.
-1
View File
@@ -55,7 +55,6 @@ 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;
}; };
+170
View File
@@ -0,0 +1,170 @@
#include "AVVideoDecoder.hpp"
#include <assert.h>
extern "C"
{
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
}
AVVideoDecoder::AVVideoDecoder()
{
codec_ctx_ = avcodec_alloc_context3(nullptr);
}
AVVideoDecoder::~AVVideoDecoder()
{
if (sws_ctx_)
sws_freeContext(sws_ctx_);
if (frame_)
av_frame_free(&frame_);
if (codec_ctx_)
avcodec_free_context(&codec_ctx_);
}
int AVVideoDecoder::open(Bambu_StreamInfo const &info)
{
auto codec_id = info.sub_type == AVC1 ? AV_CODEC_ID_H264 : AV_CODEC_ID_MJPEG;
auto codec = avcodec_find_decoder(codec_id);
if (codec == nullptr) {
fprintf(stderr, "AVVideoDecoder: unsupported codec!\n");
return -1; // Codec not found
}
/* open the coderc */
if (avcodec_open2(codec_ctx_, codec, nullptr) < 0) {
fprintf(stderr, "AVVideoDecoder: could not open codec\n");
return -1;
}
// Allocate an AVFrame structure
frame_ = av_frame_alloc();
if (frame_ == nullptr)
return -1;
return 0;
}
int AVVideoDecoder::decode(const Bambu_Sample &sample)
{
int ret = -1;
AVPacket *pkt = av_packet_alloc();
if (!pkt) {
return ret;
}
ret = av_new_packet(pkt, sample.size);
if (ret != 0) {
av_packet_free(&pkt);
return ret;
}
memcpy(pkt->data, sample.buffer, size_t(sample.size));
ret = avcodec_send_packet(codec_ctx_, pkt);
if (ret == 0) {
got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0;
}
av_packet_unref(pkt);
av_packet_free(&pkt);
return ret;
}
int AVVideoDecoder::flush()
{
int ret = avcodec_send_packet(codec_ctx_, nullptr);
got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0;
return ret;
}
void AVVideoDecoder::close()
{
}
bool AVVideoDecoder::toWxImage(wxImage &image, wxSize const &size2)
{
if (!got_frame_)
return false;
auto size1 = size2;
if (!size1.IsFullySpecified())
size1 = {frame_->width, frame_->height };
auto size = size1;
if (size.GetWidth() & 0x0f) {
size.SetWidth((size.GetWidth() & ~0x0f) + 0x10);
if (size.GetWidth() != width_) {
std::fill(bits_.begin(), bits_.end(), 0);
width_ = size.GetWidth();
}
}
AVPixelFormat wxFmt = AV_PIX_FMT_RGB24;
sws_ctx_ = sws_getCachedContext(sws_ctx_,
frame_->width, frame_->height, AVPixelFormat(frame_->format),
size1.GetWidth(), size1.GetHeight(), wxFmt,
SWS_GAUSS,
nullptr, nullptr, nullptr);
if (sws_ctx_ == nullptr)
return false;
int length = size.GetWidth() * size.GetHeight() * 3;
if (bits_.size() < length)
bits_.resize(length);
uint8_t * datas[] = { bits_.data() };
int strides[] = { size.GetWidth() * 3 };
int result_h = sws_scale(sws_ctx_, frame_->data, frame_->linesize, 0, frame_->height, datas, strides);
if (result_h != size.GetHeight()) {
return false;
}
// Copy: the frame outlives this decoder and is painted by the GUI thread while the
// next sws_scale is already overwriting bits_, so it must own its pixels. The Windows
// path below needs no equivalent, wxBitmap copies the bits into GDI.
image = wxImage(size.GetWidth(), size.GetHeight(), bits_.data(), true).Copy();
if (!image.IsOk()) {
fprintf(stderr, "AVVideoDecoder: image not ok %dx%d\n", size.GetWidth(), size.GetHeight());
return false;
}
return true;
}
bool AVVideoDecoder::toWxBitmap(wxBitmap &bitmap, wxSize const &size2)
{
if (!got_frame_)
return false;
auto size1 = size2;
if (!size1.IsFullySpecified())
size1 = {frame_->width, frame_->height };
auto size = size1;
if (size.GetWidth() & 0x0f) {
size.SetWidth((size.GetWidth() & ~0x0f) + 0x10);
if (size.GetWidth() != width_) {
std::fill(bits_.begin(), bits_.end(), 0);
width_ = size.GetWidth();
}
}
AVPixelFormat wxFmt = AV_PIX_FMT_RGB32;
sws_ctx_ = sws_getCachedContext(sws_ctx_,
frame_->width, frame_->height, AVPixelFormat(frame_->format),
size1.GetWidth(), size1.GetHeight(), wxFmt,
SWS_GAUSS,
nullptr, nullptr, nullptr);
if (sws_ctx_ == nullptr)
return false;
int length = size.GetWidth() * size.GetHeight() * 4;
if (bits_.size() < length)
bits_.resize(length);
uint8_t *datas[] = { bits_.data() };
int strides[] = { size.GetWidth() * 4 };
int result_h = sws_scale(sws_ctx_, frame_->data, frame_->linesize, 0, frame_->height, datas, strides);
if (result_h != size.GetHeight()) {
fprintf(stderr, "AVVideoDecoder: result_h %d %d\n", result_h, size.GetHeight());
return false;
}
bitmap = wxBitmap((char const *) bits_.data(), size.GetWidth(), size.GetHeight(), 32);
assert(bitmap.IsOk());
if (!bitmap.IsOk()) {
fprintf(stderr, "AVVideoDecoder: bitmap not ok %dx%d\n", size.GetWidth(), size.GetHeight());
return false;
}
return true;
}
+46
View File
@@ -0,0 +1,46 @@
#ifndef AVVIDEODECODER_HPP
#define AVVIDEODECODER_HPP
#include "Printer/BambuTunnel.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}
#include <vector>
#include <wx/bitmap.h>
#include <wx/gdicmn.h>
#include <wx/image.h>
class wxBitmap;
class AVVideoDecoder
{
public:
AVVideoDecoder();
~AVVideoDecoder();
public:
int open(Bambu_StreamInfo const &info);
int decode(Bambu_Sample const &sample);
int flush();
void close();
bool toWxImage(wxImage &image, wxSize const &size);
bool toWxBitmap(wxBitmap &bitmap, wxSize const & size);
private:
AVCodecContext *codec_ctx_ = nullptr;
AVFrame * frame_ = nullptr;
SwsContext * sws_ctx_ = nullptr;
bool got_frame_ = false;
int width_ { 0 }; // scale result width
std::vector<uint8_t> bits_;
};
#endif // AVVIDEODECODER_HPP
-1
View File
@@ -60,7 +60,6 @@ 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();
-1
View File
@@ -93,7 +93,6 @@ 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;
-30
View File
@@ -1,30 +0,0 @@
//
// BambuPlayer.h
// BambuPlayer
//
// Created by cmguo on 2021/12/6.
//
#import <Foundation/Foundation.h>
#import <AVFoundation/AVSampleBufferDisplayLayer.h>
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@interface BambuPlayer : NSObject
+ (void) initialize;
- (instancetype) initWithDisplayLayer: (AVSampleBufferDisplayLayer*) layer;
- (instancetype) initWithImageView: (NSView*) view;
- (int) open: (char const *) url;
- (NSSize) videoSize;
- (int) play;
- (void) stop;
- (void) close;
- (void) setLogger: (void (*)(void const * context, int level, char const * msg)) logger withContext: (void const *) context;
@end
NS_ASSUME_NONNULL_END
-2
View File
@@ -74,12 +74,10 @@ 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;
+20 -17
View File
@@ -16,24 +16,27 @@ 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()
{ {
// Dynamic toolhead display names from JSON config — xgettext cannot scan these // Possible runtime values of tool_head_display_names, marked for extraction.
L("Main Extruder"); L("Main extruder"); L("main extruder"); static const char *const markers[] = {
L("Auxiliary Extruder"); L("Auxiliary extruder"); L("auxiliary extruder"); L("Main Extruder"), L("Main extruder"), L("main extruder"),
L("Left Extruder"); L("Left extruder"); L("left extruder"); L("Auxiliary Extruder"), L("Auxiliary extruder"), L("auxiliary extruder"),
L("Right Extruder"); L("Right extruder"); L("right extruder"); L("Left Extruder"), L("Left extruder"), L("left extruder"),
L("Main Nozzle"); L("Main nozzle"); L("main nozzle"); L("Right Extruder"), L("Right extruder"), L("right extruder"),
L("Auxiliary Nozzle"); L("Auxiliary nozzle"); L("auxiliary nozzle"); L("Main Nozzle"), L("Main nozzle"), L("main nozzle"),
L("Left Nozzle"); L("Left nozzle"); L("left nozzle"); L("Auxiliary Nozzle"), L("Auxiliary nozzle"), L("auxiliary nozzle"),
L("Right Nozzle"); L("Right nozzle"); L("right nozzle"); L("Left Nozzle"), L("Left nozzle"), L("left nozzle"),
L("Main Hotend"); L("Main hotend"); L("main hotend"); L("Right Nozzle"), L("Right nozzle"), L("right nozzle"),
L("Auxiliary Hotend"); L("Auxiliary hotend"); L("auxiliary hotend"); L("Main Hotend"), L("Main hotend"), L("main hotend"),
L("Left Hotend"); L("Left hotend"); L("left hotend"); L("Auxiliary Hotend"), L("Auxiliary hotend"), L("auxiliary hotend"),
L("Right Hotend"); L("Right hotend"); L("right hotend"); L("Left Hotend"), L("Left hotend"), L("left 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 = "";
@@ -199,7 +199,6 @@ 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};
@@ -122,7 +122,6 @@ 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;
@@ -154,7 +153,6 @@ 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;
@@ -266,7 +266,7 @@ void wgtDeviceNozzleRackSelect::OnNozzleItemSelected(wxCommandEvent &evt)
} }
auto *item = dynamic_cast<wgtDeviceNozzleRackNozzleItem *>(evt.GetEventObject()); auto *item = dynamic_cast<wgtDeviceNozzleRackNozzleItem *>(evt.GetEventObject());
if (item; auto ptr = m_nozzle_rack.lock()) { if (auto ptr = m_nozzle_rack.lock(); item && ptr) {
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));
-1
View File
@@ -72,7 +72,6 @@ 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;
-1
View File
@@ -327,7 +327,6 @@ 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;
@@ -86,7 +86,6 @@ 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.
-5
View File
@@ -176,7 +176,6 @@ 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;
@@ -201,10 +200,6 @@ 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;
+34 -10
View File
@@ -2482,6 +2482,19 @@ static const ImWchar ranges_keyboard_shortcuts[] =
}; };
#endif // __APPLE__ #endif // __APPLE__
// Names drawn through the atlas come from file names and CAD data, not from the UI language.
// GetGlyphRangesDefault() already gives every language the CJK ideographs, which is why a
// Chinese file name renders under an English UI; these are the alphabetic scripts it omits.
// Codepoints the font lacks are skipped at build time, so only existing glyphs cost anything.
static const ImWchar ranges_language_independent[] =
{
0x0100, 0x024F, // Latin Extended-A and Extended-B
0x0370, 0x03FF, // Greek and Coptic
0x0400, 0x04FF, // Cyrillic
0x1E00, 0x1EFF, // Latin Extended Additional (Vietnamese)
0,
};
std::vector<unsigned char> ImGuiWrapper::load_svg(const std::string& bitmap_name, unsigned target_width, unsigned target_height, unsigned *outwidth, unsigned *outheight) std::vector<unsigned char> ImGuiWrapper::load_svg(const std::string& bitmap_name, unsigned target_width, unsigned target_height, unsigned *outwidth, unsigned *outheight)
{ {
@@ -2792,6 +2805,7 @@ void ImGuiWrapper::init_font(bool compress)
ImFontAtlas::GlyphRangesBuilder builder; ImFontAtlas::GlyphRangesBuilder builder;
builder.AddRanges(m_glyph_ranges); builder.AddRanges(m_glyph_ranges);
builder.AddRanges(ImGui::GetIO().Fonts->GetGlyphRangesDefault()); builder.AddRanges(ImGui::GetIO().Fonts->GetGlyphRangesDefault());
builder.AddRanges(ranges_language_independent);
#ifdef __APPLE__ #ifdef __APPLE__
if (m_font_cjk) if (m_font_cjk)
// Apple keyboard shortcuts are only contained in the CJK fonts. // Apple keyboard shortcuts are only contained in the CJK fonts.
@@ -2813,12 +2827,17 @@ void ImGuiWrapper::init_font(bool compress)
// Orca: temp fix for Korean font // Orca: temp fix for Korean font
auto font_name_regular = "HarmonyOS_Sans_SC_Regular.ttf"; auto font_name_regular = "HarmonyOS_Sans_SC_Regular.ttf";
auto font_name_bold = "HarmonyOS_Sans_SC_Bold.ttf"; auto font_name_bold = "HarmonyOS_Sans_SC_Bold.ttf";
// The Korean and Thai fonts cover their own script and little else, so they need the
// default font merged in behind them to reach the full range.
bool needs_glyph_fallback = false;
if(m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesKorean()) { if(m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesKorean()) {
font_name_regular = "NanumGothic-Regular.ttf"; font_name_regular = "NanumGothic-Regular.ttf";
font_name_bold = "NanumGothic-Bold.ttf"; font_name_bold = "NanumGothic-Bold.ttf";
needs_glyph_fallback = true;
} else if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) { } else if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) {
font_name_regular = "Sarabun-Medium.ttf"; font_name_regular = "Sarabun-Medium.ttf";
font_name_bold = "Sarabun-SemiBold.ttf"; font_name_bold = "Sarabun-SemiBold.ttf";
needs_glyph_fallback = true;
} }
default_font = io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/" + font_name_regular).c_str(), m_font_size, &cfg, ranges.Data); default_font = io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/" + font_name_regular).c_str(), m_font_size, &cfg, ranges.Data);
if (default_font == nullptr) { if (default_font == nullptr) {
@@ -2828,11 +2847,12 @@ void ImGuiWrapper::init_font(bool compress)
} }
} }
if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) { // A merged font only supplies glyphs the font ahead of it lacks, so this fills the gaps
// without restyling anything the script font already covers.
if (needs_glyph_fallback) {
ImFontConfig fallback_cfg = cfg; ImFontConfig fallback_cfg = cfg;
fallback_cfg.MergeMode = true; fallback_cfg.MergeMode = true;
static constexpr ImWchar celsius_range[] = { 0x2103, 0x2103, 0 }; io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf").c_str(), m_font_size, &fallback_cfg, ranges.Data);
io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf").c_str(), m_font_size, &fallback_cfg, celsius_range);
} }
bold_font = io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/" + font_name_bold).c_str(), m_font_size, &cfg, ranges.Data); bold_font = io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/" + font_name_bold).c_str(), m_font_size, &cfg, ranges.Data);
@@ -2841,11 +2861,10 @@ void ImGuiWrapper::init_font(bool compress)
if (bold_font == nullptr) { throw Slic3r::RuntimeError("ImGui: Could not load deafult font"); } if (bold_font == nullptr) { throw Slic3r::RuntimeError("ImGui: Could not load deafult font"); }
} }
if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) { if (needs_glyph_fallback) {
ImFontConfig fallback_cfg = cfg; ImFontConfig fallback_cfg = cfg;
fallback_cfg.MergeMode = true; fallback_cfg.MergeMode = true;
static constexpr ImWchar celsius_range[] = { 0x2103, 0x2103, 0 }; io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Bold.ttf").c_str(), m_font_size, &fallback_cfg, ranges.Data);
io.Fonts->AddFontFromFileTTF((Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Bold.ttf").c_str(), m_font_size, &fallback_cfg, celsius_range);
} }
if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) { if (m_glyph_ranges == ImGui::GetIO().Fonts->GetGlyphRangesThai()) {
@@ -2897,13 +2916,18 @@ void ImGuiWrapper::init_font(bool compress)
glsafe(::glGetIntegerv(GL_MAX_TEXTURE_SIZE, &gl_max_tex_size)); glsafe(::glGetIntegerv(GL_MAX_TEXTURE_SIZE, &gl_max_tex_size));
constexpr int max_retries = 6; constexpr int max_retries = 6;
for (int attempt = 0; attempt < max_retries && io.Fonts->TexHeight > gl_max_tex_size; ++attempt) { for (int attempt = 0; attempt < max_retries && io.Fonts->TexHeight > gl_max_tex_size; ++attempt) {
io.Fonts->TexDesiredWidth = (io.Fonts->TexDesiredWidth > 0 ? io.Fonts->TexDesiredWidth : io.Fonts->TexWidth) * 2; const int width = io.Fonts->TexDesiredWidth > 0 ? io.Fonts->TexDesiredWidth : io.Fonts->TexWidth;
// Both dimensions share the same limit, so widening past it would only trade an
// illegal height for an illegal width.
if (width * 2 > gl_max_tex_size)
break;
io.Fonts->TexDesiredWidth = width * 2;
io.Fonts->Build(); io.Fonts->Build();
} }
if (io.Fonts->TexHeight > gl_max_tex_size) { if (io.Fonts->TexHeight > gl_max_tex_size) {
// Shouldn't really happen // Needs both a very large glyph set and a small GL_MAX_TEXTURE_SIZE.
BOOST_LOG_TRIVIAL(error) << "Font atlas height " << io.Fonts->TexHeight BOOST_LOG_TRIVIAL(error) << "Font atlas " << io.Fonts->TexWidth << "x" << io.Fonts->TexHeight
<< " still exceeds GL_MAX_TEXTURE_SIZE (" << gl_max_tex_size << ")" << " does not fit GL_MAX_TEXTURE_SIZE (" << gl_max_tex_size << ")"
<< " after " << max_retries << " attempts; rendering may be incomplete"; << " after " << max_retries << " attempts; rendering may be incomplete";
} }
-1
View File
@@ -61,7 +61,6 @@ 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;
+12 -27
View File
@@ -39,13 +39,14 @@ static std::map<int, std::string> error_messages = {
namespace Slic3r { namespace Slic3r {
namespace GUI { namespace GUI {
MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl2 *media_ctrl, const wxPoint &pos, const wxSize &size) MediaPlayCtrl::MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const wxPoint &pos, const wxSize &size)
: wxPanel(parent, wxID_ANY, pos, size) : wxPanel(parent, wxID_ANY, pos, size)
, m_media_ctrl(media_ctrl) , m_media_ctrl(media_ctrl)
{ {
SetLabel("MediaPlayCtrl"); SetLabel("MediaPlayCtrl");
SetBackgroundColour(*wxWHITE); SetBackgroundColour(*wxWHITE);
m_media_ctrl->Bind(wxEVT_MEDIA_STATECHANGED, &MediaPlayCtrl::onStateChanged, this); m_media_ctrl->Bind(wxEVT_MEDIA_STATECHANGED, &MediaPlayCtrl::onStateChanged, this);
m_media_ctrl->SetIdleImage(from_u8(resources_dir() + "/images/live_stream_default.png"));
m_button_play = new Button(this, "", "media_play", wxBORDER_NONE); m_button_play = new Button(this, "", "media_play", wxBORDER_NONE);
m_button_play->SetCanFocus(false); m_button_play->SetCanFocus(false);
@@ -177,13 +178,6 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj)
if (machine == m_machine) { if (machine == m_machine) {
if (m_last_state == MEDIASTATE_IDLE && IsEnabled()) if (m_last_state == MEDIASTATE_IDLE && IsEnabled())
Play(); Play();
else if (m_last_state == MEDIASTATE_LOADING && m_tutk_state == "disable"
&& m_last_user_play + wxTimeSpan::Seconds(3) < wxDateTime::Now()) {
// resend ttcode to printer
if (auto agent = wxGetApp().getAgent())
agent->get_camera_url(machine, [](auto) {}, wxGetApp().get_printer_cloud_provider());
m_last_user_play = wxDateTime::Now();
}
return; return;
} }
m_machine = machine; m_machine = machine;
@@ -715,7 +709,9 @@ void MediaPlayCtrl::media_proc()
break; break;
} }
else if (url == "<play>") { else if (url == "<play>") {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start play";
m_media_ctrl->Play(); m_media_ctrl->Play();
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: end play";
} }
else { else {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start load"; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl: start load";
@@ -830,27 +826,16 @@ bool MediaPlayCtrl::get_stream_url(std::string *url)
}} }}
void wxMediaCtrl2::DoSetSize(int x, int y, int width, int height, int sizeFlags) void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height)
{ {
#ifdef __WXMAC__ wxSize size = videoSize;
wxWindow::DoSetSize(x, y, width, height, sizeFlags); if (!size.IsFullySpecified()) size = {16, 9};
#else
wxMediaCtrl::DoSetSize(x, y, width, height, sizeFlags);
#endif
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_gtk_video_window) {
const wxSize client_size = GetClientSize();
m_gtk_video_window->SetSize(0, 0, client_size.GetWidth(), client_size.GetHeight());
}
#endif
if (sizeFlags & wxSIZE_USE_EXISTING) return;
wxSize size = m_video_size;
int maxHeight = (width * size.GetHeight() + size.GetHeight() - 1) / size.GetWidth(); int maxHeight = (width * size.GetHeight() + size.GetHeight() - 1) / size.GetWidth();
if (maxHeight != GetMaxHeight()) { if (maxHeight != ctrl->GetMaxHeight()) {
// BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2::DoSetSize: width: " << width << ", height: " << height << ", maxHeight: " << maxHeight; // BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl_OnSize: width: " << width << ", height: " << height << ", maxHeight: " << maxHeight;
SetMaxSize({-1, maxHeight}); ctrl->SetMaxSize({-1, maxHeight});
CallAfter([this] { ctrl->CallAfter([ctrl] {
if (auto p = GetParent()) { if (auto p = ctrl->GetParent()) {
p->Layout(); p->Layout();
p->Refresh(); p->Refresh();
} }
+3 -3
View File
@@ -8,7 +8,7 @@
#ifndef MediaPlayCtrl_h #ifndef MediaPlayCtrl_h
#define MediaPlayCtrl_h #define MediaPlayCtrl_h
#include "wxMediaCtrl2.h" #include "wxMediaCtrl3.h"
#include <wx/panel.h> #include <wx/panel.h>
@@ -30,7 +30,7 @@ namespace GUI {
class MediaPlayCtrl : public wxPanel class MediaPlayCtrl : public wxPanel
{ {
public: public:
MediaPlayCtrl(wxWindow *parent, wxMediaCtrl2 *media_ctrl, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize); MediaPlayCtrl(wxWindow *parent, wxMediaCtrl3 *media_ctrl, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize);
~MediaPlayCtrl(); ~MediaPlayCtrl();
@@ -75,7 +75,7 @@ private:
// token // token
std::shared_ptr<int> m_token = std::make_shared<int>(0); std::shared_ptr<int> m_token = std::make_shared<int>(0);
wxMediaCtrl2 * m_media_ctrl; wxMediaCtrl3 * m_media_ctrl;
wxMediaState m_last_state = MEDIASTATE_IDLE; wxMediaState m_last_state = MEDIASTATE_IDLE;
std::string m_machine; std::string m_machine;
int m_lan_proto = 0; int m_lan_proto = 0;
-4
View File
@@ -86,11 +86,8 @@ 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 */
@@ -101,7 +98,6 @@ 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};
-1
View File
@@ -34,7 +34,6 @@
#include "Widgets/AxisCtrlButton.hpp" #include "Widgets/AxisCtrlButton.hpp"
#include "Widgets/TextInput.hpp" #include "Widgets/TextInput.hpp"
#include "Widgets/StaticLine.hpp" #include "Widgets/StaticLine.hpp"
#include "wxMediaCtrl2.h"
#include "MediaPlayCtrl.h" #include "MediaPlayCtrl.h"
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@@ -88,7 +88,6 @@ 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 };
-1
View File
@@ -79,7 +79,6 @@ 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;
+1 -1
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]->cali_bboxes_data.first_layer_time); plate_data_item->first_layer_time = std::to_string(m_plate_list[i]->get_slice_result()->initial_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) {
-1
View File
@@ -128,7 +128,6 @@ 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;
-1
View File
@@ -33,7 +33,6 @@ 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};
+1 -1
View File
@@ -21635,7 +21635,7 @@ void Plater::show_object_info()
auto mesh_errors = p->sidebar->obj_list()->get_mesh_errors_info(&info_manifold, &non_manifold_edges); auto mesh_errors = p->sidebar->obj_list()->get_mesh_errors_info(&info_manifold, &non_manifold_edges);
if (non_manifold_edges > 0) { if (non_manifold_edges > 0) {
info_manifold += into_u8("\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh.")); info_manifold += "\n" + _L("Tips:") + "\n" + _L("Use \"Fix Model\" to repair the mesh.");
} }
info_manifold = "<Error>" + info_manifold + "</Error>"; info_manifold = "<Error>" + info_manifold + "</Error>";
-657
View File
@@ -1,657 +0,0 @@
/* bambusrc for gstreamer
* integration with proprietary Bambu Lab blob for getting raw h.264 video
*
* Copyright (C) 2023 Joshua Wise <joshua@accelerated.tech>
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* Alternatively, the contents of this file may be used under the
* GNU Lesser General Public License Version 2.1 (the "LGPL"), in
* which case the following provisions apply instead of the ones
* mentioned above:
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <gst/gst.h>
#include "gstbambusrc.h"
#include <stdio.h>
#include <unistd.h>
#ifndef EXTERNAL_GST_PLUGIN
#define BAMBU_DYNAMIC
#endif
#include "BambuTunnel.h"
#ifdef BAMBU_DYNAMIC
// From PrinterFileSystem.
#ifdef __cplusplus
extern "C"
#else
extern
#endif
BambuLib *bambulib_get();
BambuLib *_lib = NULL;
#define BAMBULIB(x) (_lib->x)
#else
#define BAMBULIB(x) (x)
#endif
GST_DEBUG_CATEGORY_STATIC (gst_bambusrc_debug);
#define GST_CAT_DEFAULT gst_bambusrc_debug
static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
GST_PAD_SRC,
GST_PAD_ALWAYS,
GST_STATIC_CAPS_ANY);
//GST_STATIC_CAPS("video/x-h264,framerate=0/1,parsed=(boolean)false,stream-format=(string)byte-stream"));
enum
{
PROP_0,
PROP_LOCATION,
};
static void gst_bambusrc_uri_handler_init (gpointer g_iface,
gpointer iface_data);
static void gst_bambusrc_finalize (GObject * gobject);
static void gst_bambusrc_dispose (GObject * gobject);
static void gst_bambusrc_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec);
static void gst_bambusrc_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec);
static GstStateChangeReturn gst_bambusrc_change_state (GstElement *
element, GstStateChange transition);
static GstFlowReturn gst_bambusrc_create (GstPushSrc * psrc,
GstBuffer ** outbuf);
static gboolean gst_bambusrc_start (GstBaseSrc * bsrc);
static gboolean gst_bambusrc_stop (GstBaseSrc * bsrc);
static gboolean gst_bambusrc_is_seekable (GstBaseSrc * bsrc);
static gboolean gst_bambusrc_query (GstBaseSrc * bsrc, GstQuery * query);
static gboolean gst_bambusrc_unlock (GstBaseSrc * bsrc);
static gboolean gst_bambusrc_unlock_stop (GstBaseSrc * bsrc);
static gboolean gst_bambusrc_set_location (GstBambuSrc * src,
const gchar * uri, GError ** error);
#define gst_bambusrc_parent_class parent_class
G_DEFINE_TYPE_WITH_CODE (GstBambuSrc, gst_bambusrc, GST_TYPE_PUSH_SRC,
G_IMPLEMENT_INTERFACE (GST_TYPE_URI_HANDLER,
gst_bambusrc_uri_handler_init));
static void
gst_bambusrc_class_init (GstBambuSrcClass * klass)
{
GObjectClass *gobject_class;
GstElementClass *gstelement_class;
GstBaseSrcClass *gstbasesrc_class;
GstPushSrcClass *gstpushsrc_class;
gobject_class = (GObjectClass *) klass;
gstelement_class = (GstElementClass *) klass;
gstbasesrc_class = (GstBaseSrcClass *) klass;
gstpushsrc_class = (GstPushSrcClass *) klass;
gobject_class->set_property = gst_bambusrc_set_property;
gobject_class->get_property = gst_bambusrc_get_property;
gobject_class->finalize = gst_bambusrc_finalize;
gobject_class->dispose = gst_bambusrc_dispose;
g_object_class_install_property (gobject_class,
PROP_LOCATION,
g_param_spec_string ("location", "Location",
"URI to pass to Bambu Lab blobs", "",
(GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
gst_element_class_add_static_pad_template (gstelement_class, &srctemplate);
gst_element_class_set_static_metadata (gstelement_class, "Bambu Lab source",
"Source/Network",
"Receive data as a client over the network using the proprietary Bambu Lab blobs",
"Joshua Wise <joshua@accelerated.tech>");
gstelement_class->change_state =
GST_DEBUG_FUNCPTR (gst_bambusrc_change_state);
gstbasesrc_class->start = GST_DEBUG_FUNCPTR (gst_bambusrc_start);
gstbasesrc_class->stop = GST_DEBUG_FUNCPTR (gst_bambusrc_stop);
gstbasesrc_class->unlock = GST_DEBUG_FUNCPTR (gst_bambusrc_unlock);
gstbasesrc_class->unlock_stop =
GST_DEBUG_FUNCPTR (gst_bambusrc_unlock_stop);
gstbasesrc_class->is_seekable =
GST_DEBUG_FUNCPTR (gst_bambusrc_is_seekable);
gstbasesrc_class->query = GST_DEBUG_FUNCPTR (gst_bambusrc_query);
gstpushsrc_class->create = GST_DEBUG_FUNCPTR (gst_bambusrc_create);
GST_DEBUG_CATEGORY_INIT (gst_bambusrc_debug, "bambusrc", 0,
"Bambu Lab src");
}
static void
gst_bambusrc_reset (GstBambuSrc * src)
{
gst_caps_replace (&src->src_caps, NULL);
if (src->tnl) {
BAMBULIB(Bambu_Close)(src->tnl);
BAMBULIB(Bambu_Destroy)(src->tnl);
src->tnl = NULL;
}
}
static void
gst_bambusrc_init (GstBambuSrc * src)
{
src->location = NULL;
src->tnl = NULL;
gst_base_src_set_automatic_eos (GST_BASE_SRC (src), FALSE);
gst_base_src_set_live(GST_BASE_SRC(src), TRUE);
gst_bambusrc_reset (src);
}
static void
gst_bambusrc_dispose (GObject * gobject)
{
GstBambuSrc *src = GST_BAMBUSRC (gobject);
GST_DEBUG_OBJECT (src, "dispose");
G_OBJECT_CLASS (parent_class)->dispose (gobject);
}
static void
gst_bambusrc_finalize (GObject * gobject)
{
GstBambuSrc *src = GST_BAMBUSRC (gobject);
GST_DEBUG_OBJECT (src, "finalize");
g_free (src->location);
if (src->tnl) {
BAMBULIB(Bambu_Close)(src->tnl);
BAMBULIB(Bambu_Destroy)(src->tnl);
}
G_OBJECT_CLASS (parent_class)->finalize (gobject);
}
static void
gst_bambusrc_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec)
{
GstBambuSrc *src = GST_BAMBUSRC (object);
switch (prop_id) {
case PROP_LOCATION:
{
const gchar *location;
location = g_value_get_string (value);
if (location == NULL) {
GST_WARNING ("location property cannot be NULL");
goto done;
}
if (!gst_bambusrc_set_location (src, location, NULL)) {
GST_WARNING ("badly formatted location");
goto done;
}
break;
}
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
done:
return;
}
static void
gst_bambusrc_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec)
{
GstBambuSrc *src = GST_BAMBUSRC (object);
switch (prop_id) {
case PROP_LOCATION:
g_value_set_string (value, src->location);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
int gst_bambu_last_error = 0;
static GstFlowReturn
gst_bambusrc_create (GstPushSrc * psrc, GstBuffer ** outbuf)
{
GstBambuSrc *src;
src = GST_BAMBUSRC (psrc);
(void) src;
GST_DEBUG_OBJECT (src, "create()");
int rv;
Bambu_Sample sample;
if (!src->tnl) {
return GST_FLOW_ERROR;
}
while ((rv = BAMBULIB(Bambu_ReadSample)(src->tnl, &sample)) == Bambu_would_block) {
GST_DEBUG_OBJECT(src, "create would block");
usleep(33333); /* 30Hz */
}
if (rv == Bambu_stream_end) {
return GST_FLOW_EOS;
}
if (rv != Bambu_success) {
gst_bambu_last_error = rv;
return GST_FLOW_ERROR;
}
#if GLIB_CHECK_VERSION(2,68,0)
gpointer sbuf = g_memdup2(sample.buffer, sample.size);
#else
gpointer sbuf = g_memdup(sample.buffer, sample.size);
#endif
*outbuf = gst_buffer_new_wrapped_full(0, sbuf, sample.size, 0, sample.size, sbuf, g_free);
/* Synthesize monotonic timestamps at the announced frame rate, anchored
* to the first frame's arrival time. The X1C's RTSPS server emits
* unreliable decode timestamps (wildly non-monotonic jumps, or sometimes
* none at all); forwarding them directly froze the pipeline after a few
* seconds. Pacing on a synthesized clock — the same trick mpv uses when
* it reports "No video PTS! Making something up." — gives smooth
* playback regardless of network jitter, and only drops late frames if
* the printer can't keep up. A snap-back resets the anchor if real
* arrival drifts more than two frame periods from the synthesized
* timeline (e.g. announced framerate was wrong).
*/
GstClock *clock = GST_ELEMENT_CLOCK(psrc);
GstClockTime base_time = gst_element_get_base_time((GstElement *)psrc);
GstClockTime running_now = GST_CLOCK_TIME_NONE;
if (clock) {
GstClockTime now = gst_clock_get_time(clock);
if (now != GST_CLOCK_TIME_NONE && now >= base_time)
running_now = now - base_time;
}
/* Adapt the period to actual inter-arrival time via EWMA. The announced
* frame_rate is unreliable on Bambu printers (X1C announces 30 but
* delivers ~28), so trusting it causes the synthesized timeline to drift
* relative to real time, which makes the sink consider frames late and
* skip pacing entirely. Measuring the real rate keeps PTS in step with
* arrival on average, so the sink can pace inside bursts while still
* tracking the printer's actual frame cadence.
*/
if (src->avg_period == 0) {
int fps = src->frame_rate > 0 ? src->frame_rate : 30;
src->avg_period = GST_SECOND / fps;
}
if (running_now != GST_CLOCK_TIME_NONE && src->last_arrival != 0) {
GstClockTimeDiff delta = GST_CLOCK_DIFF(src->last_arrival, running_now);
/* clamp to plausible video frame periods (5..200 ms) so a one-off
* burst-of-zero or long stall doesn't poison the average */
if (delta > 5 * GST_MSECOND && delta < 200 * GST_MSECOND) {
src->avg_period = (src->avg_period * 15 + (GstClockTime)delta) / 16;
}
}
src->last_arrival = (running_now != GST_CLOCK_TIME_NONE) ? running_now : src->last_arrival;
GstClockTime period = src->avg_period;
/* Lead time: schedule frames a few periods in the future of their
* arrival, so the sink has a small jitter buffer. Without this, frames
* arriving slightly later than expected land behind the running clock
* and the sink renders them immediately, producing visible stutter.
* 100ms is invisible for a live print-monitor view.
*/
const GstClockTime LEAD = 100 * GST_MSECOND;
if (!src->sttime) {
src->sttime = (running_now != GST_CLOCK_TIME_NONE) ? running_now + LEAD : LEAD;
src->frame_count = 0;
}
GstClockTime pts = src->sttime + src->frame_count * period;
/* Safety net: with the lead applied, expected drift is roughly -LEAD
* (pts sits LEAD ns ahead of running_now). Re-anchor only if the
* synthesized timeline diverges from that expectation by several frame
* periods, which indicates a real disturbance (printer paused, stream
* resumed, large fps change) rather than ordinary jitter.
*/
if (running_now != GST_CLOCK_TIME_NONE) {
GstClockTimeDiff drift = GST_CLOCK_DIFF(pts, running_now);
GstClockTimeDiff expected = -(GstClockTimeDiff)LEAD;
GstClockTimeDiff slack = (GstClockTimeDiff)(4 * period);
if (drift > expected + slack || drift < expected - slack) {
GST_DEBUG_OBJECT(src, "ts drift %" G_GINT64_FORMAT " ns; re-anchoring", drift);
src->sttime = running_now + LEAD;
src->frame_count = 0;
pts = src->sttime;
}
}
GST_BUFFER_PTS(*outbuf) = pts;
GST_BUFFER_DTS(*outbuf) = pts;
GST_BUFFER_DURATION(*outbuf) = period;
src->frame_count++;
GST_DEBUG_OBJECT(src,
"sttime:%lu, DTS:%lu, PTS: %lu~",
src->sttime, GST_BUFFER_DTS(*outbuf), GST_BUFFER_PTS(*outbuf));
return GST_FLOW_OK;
}
static void _log(void *ctx, int lvl, const char *msg) {
GstBambuSrc *src = (GstBambuSrc *) ctx;
GST_DEBUG_OBJECT(src, "bambu: %s", msg);
BAMBULIB(Bambu_FreeLogMsg)(msg);
}
static gboolean
gst_bambusrc_start (GstBaseSrc * bsrc)
{
GstBambuSrc *src = GST_BAMBUSRC (bsrc);
GST_DEBUG_OBJECT (src, "start(\"%s\")", src->location);
if (src->tnl) {
BAMBULIB(Bambu_Close)(src->tnl);
BAMBULIB(Bambu_Destroy)(src->tnl);
src->tnl = NULL;
}
#ifdef BAMBU_DYNAMIC
if (!_lib) {
_lib = bambulib_get();
if (!_lib->Bambu_Open) {
return FALSE;
}
}
#endif
if (BAMBULIB(Bambu_Create)(&src->tnl, src->location) != Bambu_success) {
return FALSE;
}
int rv = 0;
BAMBULIB(Bambu_SetLogger)(src->tnl, _log, (void *)src);
if ((rv = BAMBULIB(Bambu_Open)(src->tnl)) != Bambu_success) {
BAMBULIB(Bambu_Destroy)(src->tnl);
src->tnl = NULL;
gst_bambu_last_error = rv;
return FALSE;
}
int n = 0;
while ((rv = BAMBULIB(Bambu_StartStream)(src->tnl, 1 /* video */)) == Bambu_would_block) {
usleep(100000);
}
if (rv != Bambu_success) {
BAMBULIB(Bambu_Close)(src->tnl);
BAMBULIB(Bambu_Destroy)(src->tnl);
src->tnl = NULL;
gst_bambu_last_error = rv;
return FALSE;
}
src->video_type = AVC1;
n = BAMBULIB(Bambu_GetStreamCount)(src->tnl);
GST_INFO_OBJECT (src, "Bambu_GetStreamCount returned stream count=%d",n);
for (int i = 0; i < n; ++i) {
Bambu_StreamInfo info;
BAMBULIB(Bambu_GetStreamInfo)(src->tnl, i, &info);
GST_INFO_OBJECT (src, "stream %d type=%d, sub_type=%d", i, info.type, info.sub_type);
if (info.type == VIDE) {
src->video_type = info.sub_type;
src->frame_rate = info.format.video.frame_rate;
GST_INFO_OBJECT (src, " width %d height=%d, frame_rate=%d",
info.format.video.width, info.format.video.height, info.format.video.frame_rate);
}
}
src->sttime = 0;
src->frame_count = 0;
src->last_arrival = 0;
src->avg_period = 0;
return TRUE;
}
static gboolean
gst_bambusrc_stop (GstBaseSrc * bsrc)
{
GstBambuSrc *src;
src = GST_BAMBUSRC (bsrc);
GST_DEBUG_OBJECT (src, "stop()");
if (src->tnl) {
BAMBULIB(Bambu_Close)(src->tnl);
BAMBULIB(Bambu_Destroy)(src->tnl);
src->tnl = NULL;
}
return TRUE;
}
static GstStateChangeReturn
gst_bambusrc_change_state (GstElement * element, GstStateChange transition)
{
GstStateChangeReturn ret;
GstBambuSrc *src;
src = GST_BAMBUSRC (element);
(void) src;
switch (transition) {
case GST_STATE_CHANGE_READY_TO_NULL:
//gst_bambusrc_session_close (src);
break;
default:
break;
}
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
return ret;
}
/* Interrupt a blocking request. */
static gboolean
gst_bambusrc_unlock (GstBaseSrc * bsrc)
{
GstBambuSrc *src;
src = GST_BAMBUSRC (bsrc);
GST_DEBUG_OBJECT (src, "unlock()");
return TRUE;
}
/* Interrupt interrupt. */
static gboolean
gst_bambusrc_unlock_stop (GstBaseSrc * bsrc)
{
GstBambuSrc *src;
src = GST_BAMBUSRC (bsrc);
GST_DEBUG_OBJECT (src, "unlock_stop()");
return TRUE;
}
static gboolean
gst_bambusrc_is_seekable (GstBaseSrc * bsrc)
{
GstBambuSrc *src = GST_BAMBUSRC (bsrc);
(void) src;
return FALSE;
}
static gboolean
gst_bambusrc_query (GstBaseSrc * bsrc, GstQuery * query)
{
GstBambuSrc *src = GST_BAMBUSRC (bsrc);
gboolean ret;
GstSchedulingFlags flags;
gint minsize, maxsize, align;
switch (GST_QUERY_TYPE (query)) {
case GST_QUERY_URI:
gst_query_set_uri (query, src->location);
ret = TRUE;
break;
default:
ret = FALSE;
break;
}
if (!ret)
ret = GST_BASE_SRC_CLASS (parent_class)->query (bsrc, query);
switch (GST_QUERY_TYPE (query)) {
case GST_QUERY_SCHEDULING:
gst_query_parse_scheduling (query, &flags, &minsize, &maxsize, &align);
flags = (GstSchedulingFlags)((int)flags | (int)GST_SCHEDULING_FLAG_SEQUENTIAL);
gst_query_set_scheduling (query, flags, minsize, maxsize, align);
break;
default:
break;
}
return ret;
}
static gboolean
gst_bambusrc_set_location (GstBambuSrc * src, const gchar * uri,
GError ** error)
{
if (src->location) {
g_free (src->location);
src->location = NULL;
}
if (uri == NULL)
return FALSE;
src->location = g_strdup (uri);
return TRUE;
}
static GstURIType
gst_bambusrc_uri_get_type (GType type)
{
return GST_URI_SRC;
}
static const gchar *const *
gst_bambusrc_uri_get_protocols (GType type)
{
static const gchar *protocols[] = { "bambu", NULL };
return protocols;
}
static gchar *
gst_bambusrc_uri_get_uri (GstURIHandler * handler)
{
GstBambuSrc *src = GST_BAMBUSRC (handler);
/* FIXME: make thread-safe */
return g_strdup (src->location);
}
static gboolean
gst_bambusrc_uri_set_uri (GstURIHandler * handler, const gchar * uri,
GError ** error)
{
GstBambuSrc *src = GST_BAMBUSRC (handler);
return gst_bambusrc_set_location (src, uri, error);
}
static void
gst_bambusrc_uri_handler_init (gpointer g_iface, gpointer iface_data)
{
GstURIHandlerInterface *iface = (GstURIHandlerInterface *) g_iface;
iface->get_type = gst_bambusrc_uri_get_type;
iface->get_protocols = gst_bambusrc_uri_get_protocols;
iface->get_uri = gst_bambusrc_uri_get_uri;
iface->set_uri = gst_bambusrc_uri_set_uri;
}
static gboolean gstbambusrc_init(GstPlugin *plugin)
{
return gst_element_register(plugin, "bambusrc", GST_RANK_PRIMARY, GST_TYPE_BAMBUSRC);
}
#ifndef EXTERNAL_GST_PLUGIN
// for use inside of Bambu Slicer
void gstbambusrc_register()
{
static int did_register = 0;
if (did_register)
return;
did_register = 1;
gst_plugin_register_static(GST_VERSION_MAJOR, GST_VERSION_MINOR, "bambusrc", "Bambu Lab source", gstbambusrc_init, "0.0.1", "GPL", "BambuStudio", "BambuStudio", "https://github.com/bambulab/BambuStudio");
}
#else
#ifndef PACKAGE
#define PACKAGE "bambusrc"
#endif
GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, bambusrc, "Bambu Lab source", gstbambusrc_init, "0.0.1", "GPL", "BambuStudio", "https://github.com/bambulab/BambuStudio")
#endif
-78
View File
@@ -1,78 +0,0 @@
/* bambusrc for gstreamer
* integration with proprietary Bambu Lab blob for getting raw h.264 video
*
* Copyright (C) 2023 Joshua Wise <joshua@accelerated.tech>
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* Alternatively, the contents of this file may be used under the
* GNU Lesser General Public License Version 2.1 (the "LGPL"), in
* which case the following provisions apply instead of the ones
* mentioned above:
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#ifndef __GST_BAMBUSRC_H__
#define __GST_BAMBUSRC_H__
#include <gst/gst.h>
#include <gst/base/gstpushsrc.h>
#include <glib.h>
G_BEGIN_DECLS
#define GST_TYPE_BAMBUSRC (gst_bambusrc_get_type())
G_DECLARE_FINAL_TYPE (GstBambuSrc, gst_bambusrc,
GST, BAMBUSRC, GstPushSrc)
typedef void *Bambu_Tunnel;
struct _GstBambuSrc
{
GstPushSrc element;
GstCaps *src_caps;
gchar *location;
Bambu_Tunnel tnl;
GstClockTime sttime;
int video_type;
int frame_rate;
guint64 frame_count;
GstClockTime last_arrival;
GstClockTime avg_period;
};
extern void gstbambusrc_register();
G_END_DECLS
#endif /* __GST_BAMBUSRC_H__ */
-1
View File
@@ -63,7 +63,6 @@ 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;
-2
View File
@@ -10,8 +10,6 @@ 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;
-2
View File
@@ -181,8 +181,6 @@ 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};
-1
View File
@@ -149,7 +149,6 @@ 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 };
-2
View File
@@ -62,7 +62,6 @@ 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};
@@ -81,7 +80,6 @@ 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 };
-1
View File
@@ -87,7 +87,6 @@ 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};
+1 -1
View File
@@ -1530,7 +1530,7 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
// media_ctrl_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); // media_ctrl_panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize);
// media_ctrl_panel->SetBackgroundColour(*wxBLACK); // media_ctrl_panel->SetBackgroundColour(*wxBLACK);
// wxBoxSizer *bSizer_monitoring = new wxBoxSizer(wxVERTICAL); // wxBoxSizer *bSizer_monitoring = new wxBoxSizer(wxVERTICAL);
m_media_ctrl = new wxMediaCtrl2(this); m_media_ctrl = new wxMediaCtrl3(this);
m_media_ctrl->SetMinSize(wxSize(PAGE_MIN_WIDTH, FromDIP(288))); m_media_ctrl->SetMinSize(wxSize(PAGE_MIN_WIDTH, FromDIP(288)));
m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString); m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString);
+1 -2
View File
@@ -14,7 +14,6 @@
#include <wx/sizer.h> #include <wx/sizer.h>
#include <wx/gbsizer.h> #include <wx/gbsizer.h>
#include <wx/webrequest.h> #include <wx/webrequest.h>
#include "wxMediaCtrl2.h"
#include "MediaPlayCtrl.h" #include "MediaPlayCtrl.h"
#include "AMSSetting.hpp" #include "AMSSetting.hpp"
#include "Calibration.hpp" #include "Calibration.hpp"
@@ -443,7 +442,7 @@ protected:
wxStaticBitmap* m_camera_switch_button; wxStaticBitmap* m_camera_switch_button;
wxMediaCtrl2 * m_media_ctrl; wxMediaCtrl3 * m_media_ctrl;
MediaPlayCtrl * m_media_play_ctrl; MediaPlayCtrl * m_media_play_ctrl;
Label * m_staticText_printing; Label * m_staticText_printing;
-3
View File
@@ -307,8 +307,6 @@ 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;
@@ -326,7 +324,6 @@ 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;
+1 -1
View File
@@ -7530,7 +7530,7 @@ void Tab::delete_preset()
for (auto &preset2 : *m_presets) for (auto &preset2 : *m_presets)
if (preset2.inherits() == current_preset.name) { if (preset2.inherits() == current_preset.name) {
++count; ++count;
presets += "\n - " + preset2.name; presets += "\n - " + from_u8(preset2.name);
} }
if (count > 0) { if (count > 0) {
msg = _L("Presets inherited by other presets cannot be deleted!"); msg = _L("Presets inherited by other presets cannot be deleted!");
-2
View File
@@ -626,8 +626,6 @@ 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;
@@ -31,7 +31,6 @@ 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()
}; };
-1
View File
@@ -862,7 +862,6 @@ 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;
-1
View File
@@ -112,7 +112,6 @@ private:
AppConfig m_appconfig_new; AppConfig m_appconfig_new;
wxWebView *m_browser; wxWebView *m_browser;
wxButton * m_TestBtn;
wxString m_SectionName; wxString m_SectionName;
-4
View File
@@ -125,10 +125,6 @@ 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;
-2
View File
@@ -114,7 +114,6 @@ 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) {
@@ -171,7 +170,6 @@ 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) {
-6
View File
@@ -684,7 +684,6 @@ 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};
@@ -693,7 +692,6 @@ 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};
@@ -866,11 +864,7 @@ 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
}; };
+13 -5
View File
@@ -1014,8 +1014,12 @@ 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()) {
L("Cooling mode is suitable for printing PLA/PETG/TPU materials."); //some potential text, add i18n flags // Possible runtime values of special_cooling_text, marked for extraction.
L("Cooling mode is suitable for printing PLA/PETG/TPU materials and filters the chamber air."); static const char *const markers[] = {
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);
} }
} }
@@ -1028,9 +1032,13 @@ 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())
{ {
L_CONTEXT("Right(Aux)", "air_duct"); // Possible runtime values of func_text, marked for extraction.
L_CONTEXT("Right(Filter)", "air_duct"); static const char *const markers[] = {
L_CONTEXT("Left(Aux)", "air_duct"); L_CONTEXT("Right(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");
} }
} }
-1
View File
@@ -109,7 +109,6 @@ 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;
@@ -204,7 +204,6 @@ 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;
}; };
+3 -2
View File
@@ -111,7 +111,7 @@ int TabCtrl::AppendItem(const wxString &item,
btns.push_back(btn); btns.push_back(btn);
if (btns.size() > 1) if (btns.size() > 1)
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0}); sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, TAB_BUTTON_SPACE * 2); sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, TAB_BUTTON_SPACE);
sizer->AddStretchSpacer(1); sizer->AddStretchSpacer(1);
relayout(); relayout();
return btns.size() - 1; return btns.size() - 1;
@@ -225,8 +225,9 @@ bool TabCtrl::IsVisible(unsigned int item) const
void TabCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags) void TabCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags)
{ {
auto size = GetSize();
wxWindow::DoSetSize(x, y, width, height, sizeFlags); wxWindow::DoSetSize(x, y, width, height, sizeFlags);
if (sizeFlags & wxSIZE_USE_EXISTING) return; if (size == GetSize()) return;
relayout(); relayout();
} }
-633
View File
@@ -1,633 +0,0 @@
#include "wxMediaCtrl2.h"
#include "libslic3r/Time.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
#include "LinuxDisplayBackend.hpp"
#include <boost/filesystem/operations.hpp>
#include <string>
#ifdef __WIN32__
#include <winuser.h>
#include <versionhelpers.h>
#include <wx/msw/registry.h>
#include <shellapi.h>
#endif
#ifdef __LINUX__
#include "Printer/gstbambusrc.h"
#include <gst/gst.h> // main gstreamer header
#endif
#if defined(__LINUX__) && defined(__WXGTK__)
#include <gtk/gtk.h>
#include <wx/nativewin.h>
namespace {
bool ensure_gstreamer_initialized_for_liveview()
{
GError* error = nullptr;
if (!gst_init_check(nullptr, nullptr, &error)) {
BOOST_LOG_TRIVIAL(error) << "wxMediaCtrl2: gst_init_check failed before native Wayland liveview setup"
<< (error ? std::string(": ") + error->message : std::string());
if (error)
g_error_free(error);
return false;
}
return true;
}
bool is_gstreamer_feature_available(const char* feature)
{
if (!ensure_gstreamer_initialized_for_liveview())
return false;
GstElementFactory* factory = gst_element_factory_find(feature);
if (!factory)
return false;
gst_object_unref(factory);
return true;
}
void set_gstreamer_feature_rank(const char* feature, guint rank)
{
GstElementFactory* factory = gst_element_factory_find(feature);
if (!factory)
return;
gst_plugin_feature_set_rank(GST_PLUGIN_FEATURE(factory), rank);
gst_object_unref(factory);
}
void configure_wayland_gstreamer_liveview_path()
{
static bool configured = false;
if (configured)
return;
configured = true;
if (!ensure_gstreamer_initialized_for_liveview())
return;
// Prefer software decode for Bambu liveview on Wayland/NVIDIA, where
// zero-copy GL/DMABUF display paths can be fragile. Keep hardware
// decoders available as lower-ranked fallbacks for VAAPI/NVDEC/V4L2-only
// installations instead of passing preflight and then blocking autoplug.
set_gstreamer_feature_rank("avdec_h264", GST_RANK_PRIMARY + 300);
set_gstreamer_feature_rank("openh264dec", GST_RANK_PRIMARY + 100);
set_gstreamer_feature_rank("nvh264dec", GST_RANK_MARGINAL);
set_gstreamer_feature_rank("vaapih264dec", GST_RANK_MARGINAL);
set_gstreamer_feature_rank("vah264dec", GST_RANK_MARGINAL);
set_gstreamer_feature_rank("v4l2h264dec", GST_RANK_MARGINAL);
}
}
#endif // defined(__LINUX__) && defined(__WXGTK__)
#ifdef __LINUX__
extern "C" int gst_bambu_last_error;
class WXDLLIMPEXP_MEDIA
wxGStreamerMediaBackend : public wxMediaBackendCommonBase
{
public:
GstElement *m_playbin; // GStreamer media element
};
#endif
wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
wxMediaCtrl2::wxMediaCtrl2(wxWindow *parent)
{
#if defined(__LINUX__) && defined(__WXGTK__)
m_native_wayland = Slic3r::GUI::is_running_on_wayland();
if (m_native_wayland && is_gstreamer_feature_available("gtksink"))
configure_wayland_gstreamer_liveview_path();
else if (m_native_wayland) {
m_gtk_sink_error = _L("Native Wayland liveview requires the GStreamer GTK video sink. Please install the gtksink plugin for GStreamer, then restart OrcaSlicer.");
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: native Wayland liveview disabled because GStreamer gtksink is unavailable";
}
#endif
#ifdef __WIN32__
auto hModExe = GetModuleHandle(NULL);
// BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: GetModuleHandle " << hModExe;
auto NvOptimusEnablement = (DWORD *) GetProcAddress(hModExe, "NvOptimusEnablement");
auto AmdPowerXpressRequestHighPerformance = (int *) GetProcAddress(hModExe, "AmdPowerXpressRequestHighPerformance");
if (NvOptimusEnablement) {
// BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: NvOptimusEnablement " << *NvOptimusEnablement;
*NvOptimusEnablement = 0;
}
if (AmdPowerXpressRequestHighPerformance) {
// BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: AmdPowerXpressRequestHighPerformance " << *AmdPowerXpressRequestHighPerformance;
*AmdPowerXpressRequestHighPerformance = 0;
}
#endif
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_native_wayland)
wxControl::Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize);
else
#endif
wxMediaCtrl::Create(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxMEDIACTRLPLAYERCONTROLS_NONE);
#ifdef __LINUX__
gstbambusrc_register();
#ifdef __WXGTK__
if (m_native_wayland && m_gtk_sink_error.empty())
m_use_gtk_sink = CreateGtkSinkPlayer();
if (m_native_wayland && !m_use_gtk_sink && m_gtk_sink_error.empty())
m_gtk_sink_error = _L("Failed to initialize the native Wayland GStreamer video sink. Please check your GStreamer GTK plugin installation.");
#endif
if (!m_use_gtk_sink && m_imp) {
auto playbin = reinterpret_cast<wxGStreamerMediaBackend *>(m_imp)->m_playbin;
g_object_set(G_OBJECT(playbin),
"audio-sink", nullptr,
nullptr);
} else if (!m_use_gtk_sink) {
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: wxMediaCtrl backend is unavailable";
}
Bind(wxEVT_MEDIA_LOADED, [this](auto & e) {
m_loaded = true;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(0);
event.SetEventObject(this);
wxPostEvent(this, event);
});
#endif
}
wxMediaCtrl2::~wxMediaCtrl2()
{
#if defined(__LINUX__) && defined(__WXGTK__)
DestroyGtkSinkPlayer();
#endif
}
#if defined(__LINUX__) && defined(__WXGTK__)
bool wxMediaCtrl2::CreateGtkSinkPlayer()
{
GstElement *playbin = gst_element_factory_make("playbin", "orca-wayland-gtk-playbin");
if (!playbin)
return false;
GError *error = nullptr;
GstElement *video_sink = gst_parse_bin_from_description(
"videoconvert ! videoscale ! video/x-raw,format=BGRx ! gtksink name=orca_wayland_gtksink sync=false",
TRUE,
&error);
if (!video_sink) {
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: failed to create gtksink video bin"
<< (error ? std::string(": ") + error->message : std::string());
if (error)
g_error_free(error);
gst_object_unref(playbin);
return false;
}
GstElement *gtk_sink = gst_bin_get_by_name(GST_BIN(video_sink), "orca_wayland_gtksink");
if (!gtk_sink) {
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: failed to find gtksink in video bin";
gst_object_unref(video_sink);
gst_object_unref(playbin);
return false;
}
GtkWidget *gtk_widget = nullptr;
g_object_get(G_OBJECT(gtk_sink), "widget", &gtk_widget, nullptr);
if (!gtk_widget) {
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: gtksink did not expose a GtkWidget";
gst_object_unref(gtk_sink);
gst_object_unref(video_sink);
gst_object_unref(playbin);
return false;
}
gtk_widget_show(gtk_widget);
m_gtk_video_window = new wxNativeWindow(this, wxID_ANY, gtk_widget);
m_gtk_video_window->Show();
g_object_unref(gtk_widget);
g_object_set(G_OBJECT(playbin),
"video-sink", video_sink,
"audio-sink", nullptr,
nullptr);
gst_object_unref(video_sink);
m_gtk_playbin = playbin;
m_gtk_sink = gtk_sink;
GstBus *bus = gst_element_get_bus(playbin);
m_gtk_bus_watch_id = gst_bus_add_watch(bus, [](GstBus *, GstMessage *message, gpointer data) -> gboolean {
auto *self = static_cast<wxMediaCtrl2 *>(data);
if (!self || !self->m_gtk_playbin)
return G_SOURCE_REMOVE;
switch (GST_MESSAGE_TYPE(message)) {
case GST_MESSAGE_ERROR:
{
GError *error = nullptr;
gchar *debug = nullptr;
gst_message_parse_error(message, &error, &debug);
BOOST_LOG_TRIVIAL(warning) << "wxMediaCtrl2: gtksink pipeline error"
<< (error ? std::string(": ") + error->message : std::string())
<< (debug ? std::string(" debug: ") + debug : std::string());
if (error)
g_error_free(error);
if (debug)
g_free(debug);
self->m_error = gst_bambu_last_error ? gst_bambu_last_error : 2;
self->m_loaded = false;
self->m_gtk_state = wxMEDIASTATE_STOPPED;
self->PostGtkSinkStateEvent(self->GetId());
break;
}
case GST_MESSAGE_EOS:
self->m_loaded = false;
self->m_gtk_state = wxMEDIASTATE_STOPPED;
self->PostGtkSinkStateEvent(self->GetId());
break;
case GST_MESSAGE_STATE_CHANGED:
if (GST_MESSAGE_SRC(message) == GST_OBJECT(self->m_gtk_playbin)) {
GstState old_state;
GstState new_state;
GstState pending_state;
gst_message_parse_state_changed(message, &old_state, &new_state, &pending_state);
if (new_state == GST_STATE_PLAYING) {
self->m_loaded = true;
self->m_gtk_state = wxMEDIASTATE_PLAYING;
self->PostGtkSinkStateEvent();
} else if (new_state == GST_STATE_PAUSED && old_state < GST_STATE_PAUSED) {
// Treat only upward READY/NULL -> PAUSED as load completion.
// PLAYING -> PAUSED is a normal teardown step before NULL.
self->m_loaded = true;
self->m_gtk_state = wxMEDIASTATE_PAUSED;
self->PostGtkSinkStateEvent();
} else if (new_state <= GST_STATE_READY && old_state >= GST_STATE_PAUSED) {
self->m_loaded = false;
self->m_gtk_state = wxMEDIASTATE_STOPPED;
self->PostGtkSinkStateEvent();
}
}
break;
default:
break;
}
return G_SOURCE_CONTINUE;
}, this);
gst_object_unref(bus);
BOOST_LOG_TRIVIAL(info) << "wxMediaCtrl2: using GTK native Wayland video sink";
return true;
}
void wxMediaCtrl2::DestroyGtkSinkPlayer()
{
if (m_gtk_bus_watch_id) {
g_source_remove(m_gtk_bus_watch_id);
m_gtk_bus_watch_id = 0;
}
if (m_gtk_playbin) {
gst_element_set_state(m_gtk_playbin, GST_STATE_NULL);
}
if (m_gtk_video_window) {
m_gtk_video_window->Destroy();
m_gtk_video_window = nullptr;
}
if (m_gtk_playbin) {
gst_object_unref(m_gtk_playbin);
m_gtk_playbin = nullptr;
}
if (m_gtk_sink) {
gst_object_unref(m_gtk_sink);
m_gtk_sink = nullptr;
}
}
void wxMediaCtrl2::PostGtkSinkStateEvent(int id)
{
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(id);
event.SetEventObject(this);
wxPostEvent(this, event);
}
#endif // defined(__LINUX__) && defined(__WXGTK__)
#define CLSID_BAMBU_SOURCE L"{233E64FB-2041-4A6C-AFAB-FF9BCF83E7AA}"
void wxMediaCtrl2::Load(wxURI url)
{
#ifdef __WIN32__
InvalidateBestSize();
if (m_imp == nullptr) {
static bool notified = false;
if (!notified) CallAfter([] {
auto res = wxMessageBox(_L("Windows Media Player is required for this task! Do you want to enable 'Windows Media Player' for your operation system?"), _L("Error"), wxOK | wxCANCEL);
if (res == wxOK) {
wxString url = IsWindows10OrGreater()
? "ms-settings:optionalfeatures?activationSource=SMC-Article-14209"
: "https://support.microsoft.com/en-au/windows/get-windows-media-player-81718e0d-cfce-25b1-aee3-94596b658287";
wxExecute("cmd /c start " + url, wxEXEC_HIDE_CONSOLE);
}
});
m_error = 100;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(GetId());
event.SetEventObject(this);
wxPostEvent(this, event);
return;
}
{
wxRegKey key11(wxRegKey::HKCU, L"SOFTWARE\\Classes\\CLSID\\" CLSID_BAMBU_SOURCE L"\\InProcServer32");
wxRegKey key12(wxRegKey::HKCR, L"CLSID\\" CLSID_BAMBU_SOURCE L"\\InProcServer32");
wxString path = key11.Exists() ? key11.QueryDefaultValue()
: key12.Exists() ? key12.QueryDefaultValue() : wxString{};
wxRegKey key2(wxRegKey::HKCR, "bambu");
wxString clsid;
if (key2.Exists())
key2.QueryRawValue("Source Filter", clsid);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": clsid %1% path %2%") % clsid % path;
std::string data_dir_str = Slic3r::data_dir();
boost::filesystem::path data_dir_path(data_dir_str);
auto dll_path = data_dir_path / "plugins" / "BambuSource.dll";
if (path.empty() || !wxFile::Exists(path) || clsid != CLSID_BAMBU_SOURCE) {
if (boost::filesystem::exists(dll_path)) {
CallAfter(
[dll_path] {
int res = wxMessageBox(_L("BambuSource has not correctly been registered for media playing! Press Yes to re-register it. You will be promoted twice"), _L("Error"), wxYES_NO);
if (res == wxYES) {
std::string regContent = R"(Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\bambu]
"Source Filter"="{233E64FB-2041-4A6C-AFAB-FF9BCF83E7AA}"
)";
auto reg_path = (fs::temp_directory_path() / fs::unique_path()).replace_extension(".reg");
std::ofstream temp_reg_file(reg_path.c_str());
if (!temp_reg_file) {
return false;
}
temp_reg_file << regContent;
temp_reg_file.close();
auto sei_params = L"/q /s " + reg_path.wstring();
SHELLEXECUTEINFO sei{sizeof(sei), SEE_MASK_NOCLOSEPROCESS, NULL, L"open",
L"regedit", sei_params.c_str(),SW_HIDE,SW_HIDE};
::ShellExecuteEx(&sei);
wstring quoted_dll_path = L"\"" + dll_path.wstring() + L"\"";
SHELLEXECUTEINFO info{sizeof(info), 0, NULL, L"runas", L"regsvr32", quoted_dll_path.c_str(), SW_HIDE };
::ShellExecuteEx(&info);
fs::remove(reg_path);
}
return true;
});
} else {
CallAfter([] {
wxMessageBox(_L("Missing BambuSource component registered for media playing! Please re-install OrcaSlicer or seek community help."), _L("Error"), wxOK);
});
}
m_error = clsid != CLSID_BAMBU_SOURCE ? 101 : path.empty() ? 102 : 103;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(GetId());
event.SetEventObject(this);
wxPostEvent(this, event);
return;
}
if (path != dll_path) {
static bool notified = false;
if (!notified) CallAfter([dll_path] {
int res = wxMessageBox(_L("Using a BambuSource from a different install, video play may not work correctly! Press Yes to fix it."), _L("Warning"), wxYES_NO | wxICON_WARNING);
if (res == wxYES) {
auto path = dll_path.wstring();
if (path.find(L' ') != std::wstring::npos)
path = L"\"" + path + L"\"";
SHELLEXECUTEINFO info{sizeof(info), 0, NULL, L"open", L"regsvr32", path.c_str(), SW_HIDE};
::ShellExecuteEx(&info);
}
});
notified = true;
}
wxRegKey keyWmp(wxRegKey::HKCU, "SOFTWARE\\Microsoft\\MediaPlayer\\Player\\Extensions\\.");
keyWmp.Create();
long permissions = 0;
if (keyWmp.HasValue("Permissions"))
keyWmp.QueryValue("Permissions", &permissions);
if ((permissions & 32) == 0) {
permissions |= 32;
keyWmp.SetValue("Permissions", permissions);
}
}
url = wxURI(url.BuildURI().append("&hwnd=").append(boost::lexical_cast<std::string>(GetHandle())).append("&tid=").append(
boost::lexical_cast<std::string>(GetCurrentThreadId())));
#endif
#ifdef __WXGTK3__
GstElementFactory *factory;
int hasplugins = 1;
factory = gst_element_factory_find("h264parse");
if (!factory) {
hasplugins = 0;
} else {
gst_object_unref(factory);
}
factory = gst_element_factory_find("openh264dec");
if (!factory) {
factory = gst_element_factory_find("avdec_h264");
}
if (!factory) {
factory = gst_element_factory_find("vaapih264dec");
}
if (!factory) {
factory = gst_element_factory_find("vah264dec");
}
if (!factory) {
factory = gst_element_factory_find("nvh264dec");
}
if (!factory) {
factory = gst_element_factory_find("v4l2h264dec");
}
if (!factory) {
hasplugins = 0;
} else {
gst_object_unref(factory);
}
if (!hasplugins) {
CallAfter([] {
wxMessageBox(_L("Your system is missing H.264 codecs for GStreamer, which are required to play video. (Try installing the gstreamer1.0-plugins-bad or gstreamer1.0-libav packages, then restart Orca Slicer?)"), _L("Error"), wxOK);
});
m_error = 101;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(GetId());
event.SetEventObject(this);
wxPostEvent(this, event);
return;
}
wxLog::EnableLogging(false);
#endif
m_error = 0;
m_loaded = false;
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_use_gtk_sink && m_gtk_playbin) {
const std::string uri = std::string(url.BuildURI().ToUTF8().data());
gst_element_set_state(m_gtk_playbin, GST_STATE_NULL);
g_object_set(G_OBJECT(m_gtk_playbin), "uri", uri.c_str(), nullptr);
m_gtk_state = wxMEDIASTATE_STOPPED;
GstStateChangeReturn state = gst_element_set_state(m_gtk_playbin, GST_STATE_PAUSED);
if (state == GST_STATE_CHANGE_FAILURE) {
m_error = gst_bambu_last_error ? gst_bambu_last_error : 2;
PostGtkSinkStateEvent(GetId());
}
return;
}
if (!m_imp) {
m_error = m_native_wayland && !m_gtk_sink_error.empty() ? 104 : 100;
m_loaded = false;
if (m_native_wayland && !m_gtk_sink_error.empty() && !m_gtk_sink_error_notified) {
m_gtk_sink_error_notified = true;
const wxString message = m_gtk_sink_error;
CallAfter([message] {
wxMessageBox(message, _L("Error"), wxOK);
});
}
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(GetId());
event.SetEventObject(this);
wxPostEvent(this, event);
return;
}
#endif
wxMediaCtrl::Load(url);
}
void wxMediaCtrl2::Play()
{
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_use_gtk_sink && m_gtk_playbin) {
GstStateChangeReturn state = gst_element_set_state(m_gtk_playbin, GST_STATE_PLAYING);
if (state == GST_STATE_CHANGE_FAILURE) {
m_error = gst_bambu_last_error ? gst_bambu_last_error : 2;
m_gtk_state = wxMEDIASTATE_STOPPED;
PostGtkSinkStateEvent(GetId());
}
return;
}
if (!m_imp) {
m_error = m_native_wayland && !m_gtk_sink_error.empty() ? 104 : 100;
if (m_native_wayland && !m_gtk_sink_error.empty() && !m_gtk_sink_error_notified) {
m_gtk_sink_error_notified = true;
const wxString message = m_gtk_sink_error;
CallAfter([message] {
wxMessageBox(message, _L("Error"), wxOK);
});
}
PostGtkSinkStateEvent(GetId());
return;
}
#endif
wxMediaCtrl::Play();
}
void wxMediaCtrl2::Stop()
{
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_use_gtk_sink && m_gtk_playbin) {
gst_element_set_state(m_gtk_playbin, GST_STATE_NULL);
m_gtk_state = wxMEDIASTATE_STOPPED;
m_loaded = false;
PostGtkSinkStateEvent(0);
return;
}
if (!m_imp)
return;
#endif
wxMediaCtrl::Stop();
}
wxMediaState wxMediaCtrl2::GetState()
{
#if defined(__LINUX__) && defined(__WXGTK__)
if (m_use_gtk_sink && m_gtk_playbin)
return m_gtk_state;
if (!m_imp)
return wxMEDIASTATE_STOPPED;
#endif
return wxMediaCtrl::GetState();
}
int wxMediaCtrl2::GetLastError() const
{
#ifdef __LINUX__
#ifdef __WXGTK__
if (m_use_gtk_sink && m_error)
return m_error;
#endif
if (m_error)
return m_error;
return gst_bambu_last_error;
#else
return m_error;
#endif
}
wxSize wxMediaCtrl2::GetVideoSize() const
{
#ifdef __LINUX__
// Gstreamer doesn't give us a VideoSize until we're playing, which
// confuses the MediaPlayCtrl into claiming that it is stuck
// "Loading...". Fake it out for now.
return m_loaded ? wxSize(1280, 720) : wxSize{};
#else
wxSize size = m_imp ? m_imp->GetVideoSize() : wxSize(0, 0);
if (size.GetWidth() > 0)
const_cast<wxSize&>(m_video_size) = size;
return size;
#endif
}
wxSize wxMediaCtrl2::DoGetBestSize() const
{
return {-1, -1};
}
#ifdef __WIN32__
WXLRESULT wxMediaCtrl2::MSWWindowProc(WXUINT nMsg,
WXWPARAM wParam,
WXLPARAM lParam)
{
// The stream source sends WM_USER+1000 with a synchronous SendMessage from its own threads,
// so this runs re-entrantly on the UI thread at whatever message-retrieval point the player
// happens to be in - often nested inside an Orca log statement. Never BOOST_LOG_TRIVIAL here:
// boost::log is not re-entrant on one thread, and doing so corrupted its per-thread record
// state, crashing later in unrelated places (the player, the log filter, a plug-in heap free).
// Post the string out (as the stat branch does) and log it on a clean stack instead.
if (nMsg == WM_USER + 1000) {
wxString msg((wchar_t const *) lParam);
if (wParam == 1) {
if (msg.EndsWith("]")) {
int n = msg.find_last_of('[');
if (n != wxString::npos) {
long val = 0;
if (msg.SubString(n + 1, msg.Length() - 2).ToLong(&val))
m_error = (int) val;
}
} else if (msg.Contains("stat_log")) {
wxCommandEvent evt(EVT_MEDIA_CTRL_STAT);
evt.SetEventObject(this);
evt.SetString(msg.Mid(msg.Find(' ') + 1));
wxPostEvent(this, evt);
}
}
return 0;
}
return wxMediaCtrl::MSWWindowProc(nMsg, wParam, lParam);
}
#endif
-117
View File
@@ -1,117 +0,0 @@
//
// wxMediaCtrl2.h
// libslic3r_gui
//
// Created by cmguo on 2021/12/7.
//
#ifndef wxMediaCtrl2_h
#define wxMediaCtrl2_h
#include "wx/uri.h"
#include "wx/mediactrl.h"
wxDECLARE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
#if defined(__LINUX__) && defined(__WXGTK__)
typedef struct _GstElement GstElement;
#endif
#ifdef __WXMAC__
class wxMediaCtrl2 : public wxWindow
{
public:
wxMediaCtrl2(wxWindow * parent);
~wxMediaCtrl2();
void Load(wxURI url);
void Play();
void Stop();
void SetIdleImage(wxString const & image);
wxMediaState GetState() const;
wxSize GetVideoSize() const;
int GetLastError() const { return m_error; }
static inline const wxMediaState MEDIASTATE_BUFFERING = static_cast<wxMediaState>(6);
protected:
void DoSetSize(int x, int y, int width, int height, int sizeFlags) override;
static void bambu_log(void const * ctx, int level, char const * msg);
void NotifyStopped();
private:
void create_player();
void * m_player = nullptr;
wxMediaState m_state = wxMEDIASTATE_STOPPED;
int m_error = 0;
wxSize m_video_size{16, 9};
};
#else
class wxMediaCtrl2 : public wxMediaCtrl
{
public:
wxMediaCtrl2(wxWindow *parent);
~wxMediaCtrl2();
void Load(wxURI url);
void Play();
void Stop();
void SetIdleImage(wxString const & image);
wxMediaState GetState();
int GetLastError() const;
wxSize GetVideoSize() const;
protected:
wxSize DoGetBestSize() const override;
void DoSetSize(int x, int y, int width, int height, int sizeFlags) override;
#ifdef __WIN32__
WXLRESULT MSWWindowProc(WXUINT nMsg,
WXWPARAM wParam,
WXLPARAM lParam) override;
#endif
private:
#if defined(__LINUX__) && defined(__WXGTK__)
bool CreateGtkSinkPlayer();
void DestroyGtkSinkPlayer();
void PostGtkSinkStateEvent(int id = 0);
bool m_native_wayland = false;
bool m_use_gtk_sink = false;
wxString m_gtk_sink_error;
bool m_gtk_sink_error_notified = false;
GstElement *m_gtk_playbin = nullptr;
GstElement *m_gtk_sink = nullptr;
unsigned int m_gtk_bus_watch_id = 0;
wxWindow *m_gtk_video_window = nullptr;
wxMediaState m_gtk_state = wxMEDIASTATE_STOPPED;
#endif
wxString m_idle_image;
int m_error = 0;
bool m_loaded = false;
wxSize m_video_size{16, 9};
};
#endif
#endif /* wxMediaCtrl2_h */
-170
View File
@@ -1,170 +0,0 @@
//
// wxMediaCtrl2.m
// OrcaSlicer
//
// Created by cmguo on 2021/12/7.
//
#import "wxMediaCtrl2.h"
#import "wx/mediactrl.h"
#include <boost/log/trivial.hpp>
#import <Foundation/Foundation.h>
#import "BambuPlayer/BambuPlayer.h"
#import "../Utils/NetworkAgent.hpp"
#include <stdlib.h>
#include <dlfcn.h>
wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
#define BAMBU_DYNAMIC
void wxMediaCtrl2::bambu_log(void const * ctx, int level, char const * msg)
{
if (level == 1) {
wxString msg2(msg);
if (msg2.EndsWith("]")) {
int n = msg2.find_last_of('[');
if (n != wxString::npos) {
long val = 0;
wxMediaCtrl2 * ctrl = (wxMediaCtrl2 *) ctx;
if (msg2.SubString(n + 1, msg2.Length() - 2).ToLong(&val))
ctrl->m_error = (int) val;
}
} else if (strstr(msg, "stat_log")) {
wxMediaCtrl2 * ctrl = (wxMediaCtrl2 *) ctx;
wxCommandEvent evt(EVT_MEDIA_CTRL_STAT);
evt.SetEventObject(ctrl);
evt.SetString(strchr(msg, ' ') + 1);
wxPostEvent(ctrl, evt);
}
} else if (level < 0) {
wxMediaCtrl2 * ctrl = (wxMediaCtrl2 *) ctx;
ctrl->NotifyStopped();
}
BOOST_LOG_TRIVIAL(info) << msg;
}
wxMediaCtrl2::wxMediaCtrl2(wxWindow * parent)
: wxWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
{
NSView * imageView = (NSView *) GetHandle();
imageView.layer = [[CALayer alloc] init];
CGColorRef color = CGColorCreateGenericRGB(0, 0, 0, 1.0f);
imageView.layer.backgroundColor = color;
CGColorRelease(color);
imageView.wantsLayer = YES;
create_player();
}
wxMediaCtrl2::~wxMediaCtrl2()
{
BambuPlayer * player = (BambuPlayer *) m_player;
[player dealloc];
}
void wxMediaCtrl2::create_player()
{
auto module = Slic3r::NetworkAgent::get_bambu_source_entry();
if (!module) {
//not ready yet
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "Network plugin not ready currently!";
return;
}
Class cls = (__bridge Class) dlsym(module, "OBJC_CLASS_$_BambuPlayer");
if (cls == nullptr) {
m_error = -2;
return;
}
NSView * imageView = (NSView *) GetHandle();
BambuPlayer * player = [cls alloc];
[player initWithImageView: imageView];
[player setLogger: bambu_log withContext: this];
m_player = player;
}
void wxMediaCtrl2::Load(wxURI url)
{
if (!m_player) {
create_player();
if (!m_player) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": create_player failed currently!";
return;
}
}
BambuPlayer * player = (BambuPlayer *) m_player;
if (player) {
[player close];
m_error = 0;
m_error = [player open: url.BuildURI().ToUTF8()];
}
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(GetId());
event.SetEventObject(this);
wxPostEvent(this, event);
}
void wxMediaCtrl2::Play()
{
if (!m_player) {
create_player();
if (!m_player) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": create_player failed currently!";
return;
}
}
BambuPlayer * player2 = (BambuPlayer *) m_player;
[player2 play];
if (m_state != wxMEDIASTATE_PLAYING) {
m_state = wxMEDIASTATE_PLAYING;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(GetId());
event.SetEventObject(this);
wxPostEvent(this, event);
}
}
void wxMediaCtrl2::Stop()
{
if (!m_player) {
create_player();
if (!m_player) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": create_player failed currently!";
return;
}
}
BambuPlayer * player2 = (BambuPlayer *) m_player;
[player2 close];
NotifyStopped();
}
void wxMediaCtrl2::NotifyStopped()
{
if (m_state != wxMEDIASTATE_STOPPED) {
m_state = wxMEDIASTATE_STOPPED;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(GetId());
event.SetEventObject(this);
wxPostEvent(this, event);
}
}
wxMediaState wxMediaCtrl2::GetState() const
{
return m_state;
}
wxSize wxMediaCtrl2::GetVideoSize() const
{
BambuPlayer * player2 = (BambuPlayer *) m_player;
if (player2) {
NSSize size = [player2 videoSize];
if (size.width > 0)
const_cast<wxSize&>(m_video_size) = {(int) size.width, (int) size.height};
return {(int) size.width, (int) size.height};
} else {
return {0, 0};
}
}
+313
View File
@@ -0,0 +1,313 @@
#include "wxMediaCtrl3.h"
#include "AVVideoDecoder.hpp"
#include "I18N.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/log/trivial.hpp>
#include <wx/dcclient.h>
#ifdef __WIN32__
#include <versionhelpers.h>
#include <wx/msw/registry.h>
#include <shellapi.h>
#endif
wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
BEGIN_EVENT_TABLE(wxMediaCtrl3, wxWindow)
// catch paint events
EVT_PAINT(wxMediaCtrl3::paintEvent)
END_EVENT_TABLE()
struct StaticBambuLib : BambuLib
{
static StaticBambuLib &get(BambuLib *);
};
wxMediaCtrl3::wxMediaCtrl3(wxWindow *parent)
: wxWindow(parent, wxID_ANY)
, BambuLib(StaticBambuLib::get(this))
, m_thread([this] { PlayThread(); })
{
SetBackgroundColour("#000001ff");
}
wxMediaCtrl3::~wxMediaCtrl3()
{
{
std::unique_lock<std::mutex> lk(m_mutex);
m_url.reset(new wxURI);
m_frame = wxImage(m_idle_image);
m_cond.notify_all();
}
m_thread.join();
}
void wxMediaCtrl3::Load(wxURI url)
{
std::unique_lock<std::mutex> lk(m_mutex);
m_video_size = wxDefaultSize;
m_error = 0;
m_url.reset(new wxURI(url));
m_cond.notify_all();
}
void wxMediaCtrl3::Play()
{
std::unique_lock<std::mutex> lk(m_mutex);
if (m_state != wxMEDIASTATE_PLAYING) {
m_state = wxMEDIASTATE_PLAYING;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(GetId());
event.SetEventObject(this);
wxPostEvent(this, event);
}
}
void wxMediaCtrl3::Stop()
{
std::unique_lock<std::mutex> lk(m_mutex);
m_url.reset();
m_frame = wxImage(m_idle_image);
NotifyStopped();
m_cond.notify_all();
Refresh();
}
void wxMediaCtrl3::SetIdleImage(wxString const &image)
{
if (m_idle_image == image)
return;
m_idle_image = image;
if (m_url == nullptr) {
std::unique_lock<std::mutex> lk(m_mutex);
m_frame = wxImage(m_idle_image);
assert(m_frame.IsOk());
Refresh();
}
}
wxMediaState wxMediaCtrl3::GetState()
{
std::unique_lock<std::mutex> lk(m_mutex);
return m_state;
}
int wxMediaCtrl3::GetLastError()
{
std::unique_lock<std::mutex> lk(m_mutex);
return m_error;
}
wxSize wxMediaCtrl3::GetVideoSize()
{
std::unique_lock<std::mutex> lk(m_mutex);
return m_video_size;
}
wxSize wxMediaCtrl3::DoGetBestSize() const
{
return {-1, -1};
}
static void adjust_frame_size(wxSize & frame, wxSize const & video, wxSize const & window)
{
if (video.x * window.y < video.y * window.x)
frame = { video.x * window.y / video.y, window.y };
else
frame = { window.x, video.y * window.x / video.x };
}
void wxMediaCtrl3::paintEvent(wxPaintEvent &evt)
{
wxPaintDC dc(this);
auto size = GetSize();
if (size.x <= 0 || size.y <= 0)
return;
std::unique_lock<std::mutex> lk(m_mutex);
if (!m_frame.IsOk())
return;
auto size2 = m_frame.GetSize();
if (size2.x != m_frame_size.x && size2.y == m_frame_size.y)
size2.x = m_frame_size.x;
auto size3 = (size - size2) / 2;
if (size2.x != size.x && size2.y != size.y) {
double scale = 1.;
if (size.x * size2.y > size.y * size2.x) {
size3 = {size.x * size2.y / size.y, size2.y};
scale = double(size.y) / size2.y;
} else {
size3 = {size2.x, size.y * size2.x / size.x};
scale = double(size.x) / size2.x;
}
dc.SetUserScale(scale, scale);
size3 = (size3 - size2) / 2;
}
dc.DrawBitmap(m_frame, size3.x, size3.y);
}
void wxMediaCtrl3::DoSetSize(int x, int y, int width, int height, int sizeFlags)
{
wxWindow::DoSetSize(x, y, width, height, sizeFlags);
if (sizeFlags == wxSIZE_USE_EXISTING) return;
wxMediaCtrl_OnSize(this, m_video_size, width, height);
std::unique_lock<std::mutex> lk(m_mutex);
adjust_frame_size(m_frame_size, m_video_size, GetSize());
Refresh();
}
void wxMediaCtrl3::bambu_log(void *ctx, int level, tchar const *msg2)
{
#ifdef _WIN32
wxString msg(msg2);
#else
wxString msg = wxString::FromUTF8(msg2);
#endif
if (level == 1) {
if (msg.EndsWith("]")) {
int n = msg.find_last_of('[');
if (n != wxString::npos) {
long val = 0;
wxMediaCtrl3 *ctrl = (wxMediaCtrl3 *) ctx;
if (msg.SubString(n + 1, msg.Length() - 2).ToLong(&val)) {
std::unique_lock<std::mutex> lk(ctrl->m_mutex);
ctrl->m_error = (int) val;
}
}
} else if (msg.Contains("stat_log")) {
wxCommandEvent evt(EVT_MEDIA_CTRL_STAT);
wxMediaCtrl3 *ctrl = (wxMediaCtrl3 *) ctx;
evt.SetEventObject(ctrl);
evt.SetString(msg.Mid(msg.Find(' ') + 1));
wxPostEvent(ctrl, evt);
}
}
BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data();
}
void wxMediaCtrl3::PlayThread()
{
using namespace std::chrono_literals;
std::shared_ptr<wxURI> url;
std::unique_lock<std::mutex> lk(m_mutex);
while (true) {
m_cond.wait(lk, [this, &url] { return m_url != url; });
url = m_url;
if (url == nullptr)
continue;
if (!url->HasScheme())
break;
lk.unlock();
Bambu_Tunnel tunnel = nullptr;
int error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8());
if (error == 0) {
Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this);
error = Bambu_Open(tunnel);
if (error == 0)
error = Bambu_would_block;
}
lk.lock();
while (error == int(Bambu_would_block)) {
m_cond.wait_for(lk, 100ms);
if (m_url != url) {
error = 1;
break;
}
lk.unlock();
error = Bambu_StartStream(tunnel, true);
lk.lock();
}
Bambu_StreamInfo info;
if (error == 0)
error = Bambu_GetStreamInfo(tunnel, 0, &info);
AVVideoDecoder decoder;
int minFrameDuration = 0;
if (error == 0) {
decoder.open(info);
m_video_size = { info.format.video.width, info.format.video.height };
adjust_frame_size(m_frame_size, m_video_size, GetSize());
minFrameDuration = 800 / info.format.video.frame_rate; // 80%
NotifyStopped();
}
Bambu_Sample sample;
while (error == 0) {
lk.unlock();
error = Bambu_ReadSample(tunnel, &sample);
lk.lock();
while (error == int(Bambu_would_block)) {
m_cond.wait_for(lk, 100ms);
if (m_url != url) {
error = 1;
break;
}
lk.unlock();
error = Bambu_ReadSample(tunnel, &sample);
lk.lock();
}
if (error == 0) {
auto frame_size = m_frame_size;
lk.unlock();
decoder.decode(sample);
#ifdef _WIN32
wxBitmap bm;
decoder.toWxBitmap(bm, frame_size);
#else
wxImage bm;
decoder.toWxImage(bm, frame_size);
#endif
lk.lock();
if (m_url != url) {
error = 1;
break;
}
if (bm.IsOk()) {
auto now = std::chrono::system_clock::now();
if (m_last_PTS && (sample.decode_time - m_last_PTS) < 30000000ULL) { // 3s
auto next_PTS_expected = m_last_PTS_expected + std::chrono::milliseconds((sample.decode_time - m_last_PTS) / 10000ULL);
// The frame is late, catch up a little
auto next_PTS_practical = m_last_PTS_practical + std::chrono::milliseconds(minFrameDuration);
auto next_PTS = std::max(next_PTS_expected, next_PTS_practical);
if(now < next_PTS)
std::this_thread::sleep_until(next_PTS);
else
next_PTS = now;
//auto text = wxString::Format(L"wxMediaCtrl3 pts diff %ld\n", std::chrono::duration_cast<std::chrono::milliseconds>(next_PTS - next_PTS_expected).count());
//OutputDebugString(text);
m_last_PTS = sample.decode_time;
m_last_PTS_expected = next_PTS_expected;
m_last_PTS_practical = next_PTS;
} else {
// Resync
m_last_PTS = sample.decode_time;
m_last_PTS_expected = now;
m_last_PTS_practical = now;
}
m_frame = bm;
}
CallAfter([this] { Refresh(); });
}
}
if (tunnel) {
lk.unlock();
Bambu_Close(tunnel);
Bambu_Destroy(tunnel);
tunnel = nullptr;
lk.lock();
}
if (m_url == url)
m_error = error;
m_frame_size = wxDefaultSize;
m_video_size = wxDefaultSize;
NotifyStopped();
}
}
void wxMediaCtrl3::NotifyStopped()
{
m_state = wxMEDIASTATE_STOPPED;
wxMediaEvent event(wxEVT_MEDIA_STATECHANGED);
event.SetId(GetId());
event.SetEventObject(this);
wxPostEvent(this, event);
}
+85
View File
@@ -0,0 +1,85 @@
//
// wxMediaCtrl3.h
// libslic3r_gui
//
// Created by cmguo on 2024/6/22.
//
#ifndef wxMediaCtrl3_h
#define wxMediaCtrl3_h
#include "wx/uri.h"
#include "wx/mediactrl.h"
wxDECLARE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height);
#define BAMBU_DYNAMIC
#include <condition_variable>
#include <thread>
#ifndef _WIN32
#include <wx/image.h>
#endif
#include "Printer/BambuTunnel.h"
class AVVideoDecoder;
class wxMediaCtrl3 : public wxWindow, BambuLib
{
public:
wxMediaCtrl3(wxWindow *parent);
~wxMediaCtrl3();
void Load(wxURI url);
void Play();
void Stop();
void SetIdleImage(wxString const & image);
wxMediaState GetState();
int GetLastError();
wxSize GetVideoSize();
protected:
DECLARE_EVENT_TABLE()
void paintEvent(wxPaintEvent &evt);
wxSize DoGetBestSize() const override;
void DoSetSize(int x, int y, int width, int height, int sizeFlags) override;
static void bambu_log(void *ctx, int level, tchar const *msg);
void PlayThread();
void NotifyStopped();
private:
wxString m_idle_image;
wxMediaState m_state = wxMEDIASTATE_STOPPED;
int m_error = 0;
wxSize m_video_size = wxDefaultSize;
wxSize m_frame_size = wxDefaultSize;
#ifdef _WIN32
wxBitmap m_frame;
#else
wxImage m_frame;
#endif
std::shared_ptr<wxURI> m_url;
std::uint64_t m_last_PTS{0};
std::chrono::system_clock::time_point m_last_PTS_expected;
std::chrono::system_clock::time_point m_last_PTS_practical;
std::mutex m_mutex;
std::condition_variable m_cond;
std::thread m_thread;
};
#endif /* wxMediaCtrl3_h */
+660 -103
View File
@@ -7,11 +7,127 @@
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <algorithm>
#include <cctype>
#include <cstdlib> #include <cstdlib>
#include <future>
#include <slic3r/GUI/BindDialog.hpp>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <unordered_map>
#include <unordered_set>
#include <utility> #include <utility>
#include <vector>
#include <wx/event.h>
#include <wx/msgdlg.h>
namespace Slic3r { namespace Slic3r {
// extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html
static const std::unordered_map<std::string, AuditEventCategory> audit_event_categories{
// fsread
{"glob.glob", AuditEventCategory::FsRead},
{"glob.glob/2", AuditEventCategory::FsRead},
{"os.fwalk", AuditEventCategory::FsRead},
{"os.getxattr", AuditEventCategory::FsRead},
{"os.listdir", AuditEventCategory::FsRead},
{"os.listdrives", AuditEventCategory::FsRead},
{"os.listmounts", AuditEventCategory::FsRead},
{"os.listvolumes", AuditEventCategory::FsRead},
{"os.listxattr", AuditEventCategory::FsRead},
{"os.scandir", AuditEventCategory::FsRead},
{"os.walk", AuditEventCategory::FsRead},
{"pathlib.Path.glob", AuditEventCategory::FsRead},
{"pathlib.Path.rglob", AuditEventCategory::FsRead},
// fsreadwrite
{"os.chflags", AuditEventCategory::FsReadWrite},
{"os.chmod", AuditEventCategory::FsReadWrite},
{"os.chown", AuditEventCategory::FsReadWrite},
{"os.removexattr", AuditEventCategory::FsReadWrite},
{"os.rename", AuditEventCategory::FsReadWrite},
{"os.setxattr", AuditEventCategory::FsReadWrite},
{"os.truncate", AuditEventCategory::FsReadWrite},
{"os.utime", AuditEventCategory::FsReadWrite},
{"shutil.chown", AuditEventCategory::FsReadWrite},
{"shutil.copymode", AuditEventCategory::FsReadWrite},
{"shutil.copystat", AuditEventCategory::FsReadWrite},
{"shutil.copyfile", AuditEventCategory::FsReadWrite},
{"shutil.copytree", AuditEventCategory::FsReadWrite},
{"shutil.make_archive", AuditEventCategory::FsReadWrite},
{"shutil.move", AuditEventCategory::FsReadWrite},
{"shutil.unpack_archive", AuditEventCategory::FsReadWrite},
// fscreate
{"os.link", AuditEventCategory::FsCreate},
{"os.mkdir", AuditEventCategory::FsCreate},
{"os.symlink", AuditEventCategory::FsCreate},
{"tempfile.mkdtemp", AuditEventCategory::FsCreate},
{"tempfile.mkstemp", AuditEventCategory::FsCreate},
{"_winapi.CreateJunction", AuditEventCategory::FsCreate},
// fsdelete
{"os.remove", AuditEventCategory::FsDelete},
{"os.rmdir", AuditEventCategory::FsDelete},
{"shutil.rmtree", AuditEventCategory::FsDelete},
// http
{"http.client.connect", AuditEventCategory::Http},
{"http.client.send", AuditEventCategory::Http},
{"urllib.Request", AuditEventCategory::Http},
// socket
{"socket.__new__", AuditEventCategory::Socket},
{"socket.bind", AuditEventCategory::Socket},
{"socket.connect", AuditEventCategory::Socket},
{"socket.getaddrinfo", AuditEventCategory::Socket},
{"socket.gethostbyaddr", AuditEventCategory::Socket},
{"socket.gethostbyname", AuditEventCategory::Socket},
{"socket.gethostname", AuditEventCategory::Socket},
{"socket.getnameinfo", AuditEventCategory::Socket},
{"socket.getservbyname", AuditEventCategory::Socket},
{"socket.getservbyport", AuditEventCategory::Socket},
{"socket.sendmsg", AuditEventCategory::Socket},
{"socket.sendto", AuditEventCategory::Socket},
// processcreate
{"os.fork", AuditEventCategory::ProcessCreate},
{"os.forkpty", AuditEventCategory::ProcessCreate},
{"os.posix_spawn", AuditEventCategory::ProcessCreate},
{"os.spawn", AuditEventCategory::ProcessCreate},
{"os.system", AuditEventCategory::ProcessCreate},
{"os.startfile", AuditEventCategory::ProcessCreate},
{"os.startfile/2", AuditEventCategory::ProcessCreate},
{"pty.spawn", AuditEventCategory::ProcessCreate},
{"subprocess.Popen", AuditEventCategory::ProcessCreate},
{"_winapi.CreateProcess", AuditEventCategory::ProcessCreate},
{"_posixsubprocess.fork_exec", AuditEventCategory::ProcessCreate},
};
// Returns the category event_name belongs to, or AuditEventCategory::None when it isn't audited.
static AuditEventCategory event_category(const std::string& event_name)
{
const auto it = audit_event_categories.find(event_name);
return it == audit_event_categories.end() ? AuditEventCategory::None : it->second;
}
// True for the categories whose targets are filesystem paths, as opposed to a network
// address or a process command line -- the deny-path and allowed-root checks only make sense
// against a path.
static bool is_fs_category(AuditEventCategory category)
{
switch (category) {
case AuditEventCategory::FsRead:
case AuditEventCategory::FsReadWrite:
case AuditEventCategory::FsCreate:
case AuditEventCategory::FsDelete:
return true;
default:
return false;
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Path safety // Path safety
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -80,22 +196,19 @@ bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boos
thread_local std::string PluginAuditManager::m_current_plugin_key = ""; thread_local std::string PluginAuditManager::m_current_plugin_key = "";
thread_local std::string PluginAuditManager::m_current_capability_name = ""; thread_local std::string PluginAuditManager::m_current_capability_name = "";
thread_local PluginAuditManager::AuditMode PluginAuditManager::m_audit_mode = PluginAuditManager::AuditMode::Loading; thread_local std::vector<AllowedRoot> PluginAuditManager::m_scoped_allowed_roots;
thread_local std::vector<boost::filesystem::path> PluginAuditManager::m_scoped_allowed_roots; thread_local bool PluginAuditManager::m_audit_denial_pending = false;
thread_local bool PluginAuditManager::m_has_last_violation = false; thread_local bool PluginAuditManager::m_has_last_violation = false;
thread_local AuditViolation PluginAuditManager::m_last_violation; thread_local AuditViolation PluginAuditManager::m_last_violation;
ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key, ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key,
const std::string& capability_name, const std::string& capability_name)
PluginAuditManager::AuditMode mode)
: m_previous_id(PluginAuditManager::instance().current_plugin()) : m_previous_id(PluginAuditManager::instance().current_plugin())
, m_previous_capability(PluginAuditManager::instance().current_capability()) , m_previous_capability(PluginAuditManager::instance().current_capability())
, m_previous_mode(PluginAuditManager::instance().audit_mode())
, m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots) , m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots)
{ {
PluginAuditManager::instance().set_current_plugin(plugin_key); PluginAuditManager::instance().set_current_plugin(plugin_key);
PluginAuditManager::instance().set_current_capability(capability_name); PluginAuditManager::instance().set_current_capability(capability_name);
PluginAuditManager::instance().set_audit_mode(mode);
PluginAuditManager::m_scoped_allowed_roots.clear(); PluginAuditManager::m_scoped_allowed_roots.clear();
} }
@@ -103,7 +216,6 @@ ScopedPluginAuditContext::~ScopedPluginAuditContext()
{ {
PluginAuditManager::instance().set_current_plugin(m_previous_id); PluginAuditManager::instance().set_current_plugin(m_previous_id);
PluginAuditManager::instance().set_current_capability(m_previous_capability); PluginAuditManager::instance().set_current_capability(m_previous_capability);
PluginAuditManager::instance().set_audit_mode(m_previous_mode);
PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots); PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots);
} }
@@ -129,23 +241,24 @@ std::string PluginAuditManager::current_capability() const { return m_current_ca
void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); } void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); }
void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root) void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root, bool allow_write)
{ {
if (root.empty()) if (root.empty())
return; return;
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_global_allowed_roots.push_back(root); m_global_allowed_roots.push_back({root, allow_write});
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string(); BOOST_LOG_TRIVIAL(info) << "[AUDIT] Global allowed root: " << root.string() << " allow_write=" << allow_write;
} }
void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root) void PluginAuditManager::add_scoped_allowed_root(const boost::filesystem::path& root, bool allow_write)
{ {
if (root.empty()) if (root.empty())
return; return;
m_scoped_allowed_roots.push_back(root); m_scoped_allowed_roots.push_back({root, allow_write});
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string(); BOOST_LOG_TRIVIAL(info) << "[AUDIT] Scoped allowed root for plugin " << current_plugin() << ": " << root.string()
<< " allow_write=" << allow_write;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -199,12 +312,65 @@ bool PluginAuditManager::is_denied_filename(const boost::filesystem::path& candi
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Audit mode // Denied path keywords
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void PluginAuditManager::set_audit_mode(AuditMode mode) { m_audit_mode = mode; } void PluginAuditManager::add_denied_path_keyword(const std::string& keyword)
{
if (keyword.empty())
return;
PluginAuditManager::AuditMode PluginAuditManager::audit_mode() const { return m_audit_mode; } std::string lower = keyword;
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return std::tolower(c); });
std::lock_guard<std::mutex> lock(m_mutex);
m_denied_path_keywords.push_back(lower);
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Denied path keyword: " << lower;
}
std::vector<std::string> PluginAuditManager::default_denied_path_keywords()
{
// Broad, categorical rules on top of the exact-name is_denied_filename registry: a plugin
// must never be able to reach a secret, a certificate, or a configuration file just because
// it happens to live inside an otherwise-allowed root (e.g. the bundled TLS client cert at
// resources_dir()/cert/..., which would become reachable the moment resources_dir() is
// granted as a read-only allowed root).
return {"secret", "cert", "conf"};
}
bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& candidate) const
{
namespace fs = boost::filesystem;
boost::system::error_code ec;
fs::path canon = fs::weakly_canonical(candidate, ec);
if (ec) {
canon = fs::absolute(candidate, ec).lexically_normal();
if (ec)
canon = candidate;
}
std::lock_guard<std::mutex> lock(m_mutex);
if (m_denied_path_keywords.empty())
return false;
for (const auto& component : canon) {
std::string name = component.string();
if (name.empty())
continue;
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); });
for (const auto& keyword : m_denied_path_keywords) {
if (name.find(keyword) != std::string::npos)
return true;
}
}
return false;
}
bool PluginAuditManager::is_denied_path(const boost::filesystem::path& candidate) const
{
return is_denied_filename(candidate) || is_denied_path_keyword(candidate);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Policy checks // Policy checks
@@ -219,20 +385,20 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
if (plugin_key.empty()) if (plugin_key.empty())
return {true, ""}; // not running inside a plugin context return {true, ""}; // not running inside a plugin context
// Denied filenames are checked first, above both the Loading exemption below and the // Denied filenames/keywords are checked before the allowed roots. The app config and the
// allowed roots. The app config and the cloud refresh token live directly inside // cloud refresh token live directly inside data_dir(), which is a global allowed root, and
// data_dir(), which is a global allowed root, and no scope ever sets Enforcing — so a // the bundled TLS client cert lives inside resources_dir(), a read-only global allowed
// deny placed any lower would be unreachable for reads. // root, so a deny placed any lower would be unreachable.
if (is_denied_filename(path)) { if (is_denied_filename(path)) {
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write
<< " plugin=" << plugin_key << " reason=denied filename"; << " plugin=" << plugin_key << " reason=denied filename";
return {false, "denied filename"}; return {false, "denied filename"};
} }
if (is_denied_path_keyword(path)) {
// During import/loading, only block writes. Python must be able to read BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << path.string() << " is_write=" << is_write
// stdlib modules and the plugin file itself during import. << " plugin=" << plugin_key << " reason=denied path keyword";
if (m_audit_mode == AuditMode::Loading && !is_write) return {false, "denied path keyword"};
return {true, ""}; }
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
fs::path candidate = path; fs::path candidate = path;
@@ -245,8 +411,11 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
candidate = absolute_candidate; candidate = absolute_candidate;
} }
// A root that doesn't allow writes only matches a read-shaped request; a write/create/
// delete-shaped one falls through to "outside allowed root" for that root even though the
// path is physically inside it.
for (const auto& root : m_scoped_allowed_roots) { for (const auto& root : m_scoped_allowed_roots) {
if (is_inside_allowed_root(candidate, root)) { if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) {
return {true, ""}; return {true, ""};
} }
} }
@@ -254,14 +423,13 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
{ {
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
for (const auto& root : m_global_allowed_roots) { for (const auto& root : m_global_allowed_roots) {
if (is_inside_allowed_root(candidate, root)) { if ((!is_write || root.allow_write) && is_inside_allowed_root(candidate, root.path)) {
return {true, ""}; return {true, ""};
} }
} }
} }
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << candidate.string() << " is_write=" << is_write BOOST_LOG_TRIVIAL(warning) << "[AUDIT] block path=" << candidate.string() << " is_write=" << is_write
<< " audit_mode=" << (m_audit_mode == AuditMode::Loading ? "Loading" : "Enforcing")
<< " plugin=" << plugin_key; << " plugin=" << plugin_key;
return {false, "outside allowed root"}; return {false, "outside allowed root"};
} }
@@ -269,19 +437,99 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
AuditDecision PluginAuditManager::check_open(const std::string& path_str, const std::string& mode) AuditDecision PluginAuditManager::check_open(const std::string& path_str, const std::string& mode)
{ {
const bool is_write = mode.find('w') != std::string::npos || mode.find('a') != std::string::npos || const bool is_write = mode.find('w') != std::string::npos || mode.find('a') != std::string::npos ||
mode.find('+') != std::string::npos; mode.find('+') != std::string::npos || mode.find('x') != std::string::npos;
return check_path_access(boost::filesystem::path(path_str), is_write); return check_path_access(boost::filesystem::path(path_str), is_write);
} }
bool PluginAuditManager::request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths)
{
if (plugin_key.empty() || paths.empty())
return true;
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
PluginInstallState state;
read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
std::vector<std::string> missing;
for (const std::string& path : paths) {
if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path) == state.permissions.fs_read.end())
missing.push_back(path);
}
if (missing.empty())
return true;
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
auto show_dialog = [&descriptor, &missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format("Plugin \"%s\" requests filesystem read access to:\n%s",
wxString::FromUTF8(descriptor.name.c_str()), requested_paths),
"Plugin permissions",
wxYES_NO | wxICON_WARNING);
return dialog.ShowModal() == wxID_YES;
};
bool granted = false;
if (wxIsMainThread()) {
granted = show_dialog();
} else {
auto result = std::make_shared<std::promise<bool>>();
auto future = result->get_future();
GUI::wxGetApp().CallAfter([result, descriptor_name = descriptor.name, missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format("Plugin \"%s\" requests filesystem read access to:\n%s",
wxString::FromUTF8(descriptor_name.c_str()), requested_paths),
"Plugin permissions",
wxYES_NO | wxICON_WARNING);
result->set_value(dialog.ShowModal() == wxID_YES);
});
granted = future.get();
}
if (!granted)
return false;
if (state.plugin_name.empty()) {
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 = true;
}
state.permissions.fs_read.insert(state.permissions.fs_read.end(), missing.begin(), missing.end());
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
}
void PluginAuditManager::report_violation(const AuditViolation& violation) void PluginAuditManager::report_violation(const AuditViolation& violation)
{ {
m_last_violation = violation; m_last_violation = violation;
m_has_last_violation = true; m_has_last_violation = true;
m_audit_denial_pending = true;
BOOST_LOG_TRIVIAL(warning) << "[AUDIT BLOCKED] plugin=" << violation.plugin_key << " event=" << violation.event_name BOOST_LOG_TRIVIAL(warning) << "[AUDIT BLOCKED] plugin=" << violation.plugin_key << " event=" << violation.event_name
<< " path=" << violation.path.string() << " reason=" << violation.reason; << " reason=" << violation.reason;
} }
bool PluginAuditManager::audit_denial_pending() const { return m_audit_denial_pending; }
void PluginAuditManager::clear_audit_denial() { m_audit_denial_pending = false; }
void PluginAuditManager::clear_last_violation() void PluginAuditManager::clear_last_violation()
{ {
m_has_last_violation = false; m_has_last_violation = false;
@@ -297,31 +545,326 @@ bool PluginAuditManager::last_violation(AuditViolation& violation) const
return true; return true;
} }
bool PluginAuditManager::has_approved_ancestor(const std::string& plugin_key,
const std::vector<std::string>& call_site_ids) const
{
if (plugin_key.empty() || call_site_ids.empty())
return false;
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_approved_call_sites.find(plugin_key);
if (it == m_approved_call_sites.end())
return false;
for (const auto& id : call_site_ids)
if (it->second.count(id))
return true;
return false;
}
void PluginAuditManager::record_approved_call_sites(const std::string& plugin_key,
const std::vector<std::string>& call_site_ids)
{
if (plugin_key.empty() || call_site_ids.empty())
return;
std::lock_guard<std::mutex> lock(m_mutex);
auto& approved = m_approved_call_sites[plugin_key];
approved.insert(call_site_ids.begin(), call_site_ids.end());
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The C-level audit hook // The C-level audit hook
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
namespace { namespace PluginAuditDetail {
PyObject* tuple_item(PyObject* args, Py_ssize_t index)
{
if (!args || !PyTuple_Check(args) || index < 0 || index >= PyTuple_GET_SIZE(args))
return nullptr;
return PyTuple_GET_ITEM(args, index);
}
std::string python_path(PyObject* object)
{
if (!object)
return {};
PyObject* path_object = PyOS_FSPath(object);
if (!path_object) {
PyErr_Clear();
return {};
}
const char* path = PyUnicode_Check(path_object) ? PyUnicode_AsUTF8(path_object) : PyBytes_AsString(path_object);
std::string result = path ? path : "";
Py_DECREF(path_object);
if (!path)
PyErr_Clear();
return result;
}
std::string python_unicode(PyObject* object)
{
if (!object || !PyUnicode_Check(object))
return {};
const char* text = PyUnicode_AsUTF8(object);
if (!text) {
PyErr_Clear();
return {};
}
return text;
}
std::string python_str(PyObject* object)
{
if (!object)
return {};
PyObject* str_object = PyObject_Str(object);
if (!str_object) {
PyErr_Clear();
return {};
}
const std::string result = python_unicode(str_object);
Py_DECREF(str_object);
return result;
}
std::vector<std::string> call_site_identities(const std::string& plugin_root)
{
std::vector<std::string> ids;
if (plugin_root.empty())
return ids;
PyFrameObject* frame = PyEval_GetFrame(); // borrowed reference
Py_XINCREF(frame); // normalize to an owned reference for the loop below
while (frame) {
PyCodeObject* code = PyFrame_GetCode(frame); // new reference
const std::string filename = python_unicode(reinterpret_cast<PyObject*>(code->co_filename));
const bool is_plugin_frame = !filename.empty() && boost::algorithm::starts_with(filename, plugin_root);
std::string id;
if (!is_plugin_frame && !filename.empty()) {
const std::string funcname = python_unicode(reinterpret_cast<PyObject*>(code->co_name));
id = filename + ":" + funcname + ":" + std::to_string(code->co_firstlineno);
}
Py_DECREF(code);
PyFrameObject* back = PyFrame_GetBack(frame); // new reference, or nullptr at the top of the stack
Py_DECREF(frame);
frame = back;
if (is_plugin_frame)
break;
if (!id.empty())
ids.push_back(std::move(id));
}
Py_XDECREF(frame); // only holds a reference here if the loop exited via break
return ids;
}
static const std::unordered_set<std::string> two_path_fs_events{
"os.rename", "os.link", "os.symlink", "shutil.copyfile",
"shutil.copytree", "shutil.copymode", "shutil.copystat", "shutil.move",
"shutil.unpack_archive", "_winapi.CreateJunction",
};
static const std::unordered_map<std::string, std::vector<Py_ssize_t>> audit_target_arg_indices{
{"http.client.connect", {1}},
{"urllib.Request", {0}},
{"socket.connect", {1}},
{"socket.bind", {1}},
{"socket.getaddrinfo", {0}},
{"socket.gethostbyname", {0}},
{"socket.gethostbyaddr", {0}},
{"socket.getnameinfo", {0}},
{"socket.getservbyname", {0}},
{"socket.getservbyport", {0}},
{"os.system", {0}},
{"subprocess.Popen", {1, 0}},
{"os.posix_spawn", {1, 0}},
{"os.spawn", {2, 1}},
{"os.startfile", {0}},
{"pty.spawn", {0}},
{"_winapi.CreateProcess", {1, 0}},
{"_posixsubprocess.fork_exec", {0}},
};
AuditEventCategory open_category(PyObject* args)
{
const std::string mode = python_unicode(tuple_item(args, 1));
if (!mode.empty() && mode.find_first_of("wax+") == std::string::npos)
return AuditEventCategory::FsRead;
return AuditEventCategory::FsReadWrite;
}
std::vector<std::string> audit_targets(const std::string& event_name, AuditEventCategory category, PyObject* args)
{
std::vector<std::string> targets;
switch (category) {
case AuditEventCategory::FsRead:
case AuditEventCategory::FsReadWrite:
case AuditEventCategory::FsCreate:
case AuditEventCategory::FsDelete: {
const Py_ssize_t path_count = two_path_fs_events.count(event_name) ? 2 : 1;
for (Py_ssize_t index = 0; index < path_count; ++index) {
const std::string path = python_path(tuple_item(args, index));
if (!path.empty())
targets.push_back(path);
}
return targets;
}
default:
break;
}
const auto it = audit_target_arg_indices.find(event_name);
if (it != audit_target_arg_indices.end()) {
for (const Py_ssize_t index : it->second) {
std::string value = python_str(tuple_item(args, index));
if (!value.empty()) {
targets.push_back(std::move(value));
break;
}
}
}
return targets;
}
std::vector<std::string>* permission_list_for(AuditEventCategory category, PluginPermissions& permissions)
{
switch (category) {
case AuditEventCategory::FsRead: return &permissions.fs_read;
case AuditEventCategory::FsReadWrite: return &permissions.fs_readwrite;
case AuditEventCategory::Http: return &permissions.network_http;
case AuditEventCategory::Socket: return &permissions.network_socket;
case AuditEventCategory::ProcessCreate: return &permissions.process;
default: return nullptr;
}
}
bool has_permission(const std::vector<std::string>& granted, const std::string& target)
{
return std::find(granted.begin(), granted.end(), target) != granted.end();
}
bool persist_permission(const std::string& plugin_key,
PluginInstallState& state,
std::vector<std::string>& permission_list,
const std::string& target)
{
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
// A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata
// needed by the writer so clicking Yes still creates the JSON file.
if (state.plugin_name.empty()) {
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 = true;
}
if (!has_permission(permission_list, target))
permission_list.push_back(target);
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
}
// Records a blocked event and raises PermissionError in the calling interpreter. Returns -1
// so an event branch can `return report_denied(...)` directly.
int report_denied(PluginAuditManager& mgr, int report_denied(PluginAuditManager& mgr,
const std::string& event_name, const std::string& event_name,
const boost::filesystem::path& path,
const AuditDecision& decision) const AuditDecision& decision)
{ {
AuditViolation violation; AuditViolation violation;
violation.plugin_key = mgr.current_plugin(); violation.plugin_key = mgr.current_plugin();
violation.event_name = event_name; violation.event_name = event_name;
violation.path = path;
violation.reason = decision.reason; violation.reason = decision.reason;
mgr.report_violation(violation); mgr.report_violation(violation);
PyErr_SetString(PyExc_PermissionError, "Plugin attempted to access a blocked file path"); PyErr_SetString(PyExc_PermissionError, "Plugin attempted an audited operation without permission");
return -1; return -1;
} }
} // namespace wxString audit_message(AuditEventCategory category, const wxString& plugin_name, const wxString& event_name,
const wxString& target_list)
{
if (target_list.IsEmpty())
return wxString::Format(
_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\".\n\n"
"This operation does not expose a target the audit hook can display."),
plugin_name, event_name);
switch (category) {
case AuditEventCategory::FsRead:
return wxString::Format(_L("Plugin \"%s\" is requesting to read the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::FsReadWrite:
return wxString::Format(_L("Plugin \"%s\" is requesting to read/write the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::FsCreate:
return wxString::Format(_L("Plugin \"%s\" is requesting to create the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::FsDelete:
return wxString::Format(_L("Plugin \"%s\" is requesting to delete the following file(s):\n%s"), plugin_name, target_list);
case AuditEventCategory::Http:
return wxString::Format(_L("Plugin \"%s\" is requesting to make an HTTP request to:\n%s"), plugin_name, target_list);
case AuditEventCategory::Socket:
return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list);
case AuditEventCategory::ProcessCreate:
return wxString::Format(_L("Plugin \"%s\" is requesting to run the following command(s):\n%s"), plugin_name, target_list);
default:
return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name);
}
}
int decide_audited_event(PluginAuditManager& mgr,
PluginInstallState& state,
const std::string& plugin_key,
const std::string& plugin_name,
const std::string& event_name,
AuditEventCategory category,
const std::vector<std::string>& targets,
std::vector<std::string>* permission_list,
const std::vector<std::string>& call_site_ids)
{
std::vector<std::string> unresolved = targets;
if (permission_list) {
unresolved.clear();
for (const auto& target : targets)
if (!has_permission(*permission_list, target))
unresolved.push_back(target);
if (!targets.empty() && unresolved.empty())
return 0;
}
wxString target_list;
for (const auto& target : unresolved)
target_list += wxString::FromUTF8(target.c_str()) + "\n";
wxMessageDialog dialog(nullptr,
audit_message(category, wxString::FromUTF8(plugin_name.c_str()),
wxString::FromUTF8(event_name.c_str()), target_list),
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
if (dialog.ShowModal() != wxID_YES)
return report_denied(mgr, event_name, {false, "audit permission required"});
if (permission_list)
for (const auto& target : unresolved)
persist_permission(plugin_key, state, *permission_list, target);
mgr.record_approved_call_sites(plugin_key, call_site_ids);
return 0;
}
} // namespace PluginAuditDetail
int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data) int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data)
{ {
@@ -331,94 +874,95 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
std::string event_name(event ? event : ""); std::string event_name(event ? event : "");
if (event_name.empty())
return 0;
// Verbose logging of every audit event (can be noisy) // Verbose logging of every audit event (can be noisy)
if (mgr->verbose_events) { if (mgr->verbose_events) {
BOOST_LOG_TRIVIAL(debug) << "[AUDIT EVENT] " << event_name; BOOST_LOG_TRIVIAL(debug) << "[AUDIT EVENT] " << event_name;
} }
// extensive list of audit events can be found at https://docs.python.org/3/library/audit_events.html if (mgr->current_plugin().empty()) {
BOOST_LOG_TRIVIAL(trace) << "[AUDIT] event=" << event_name << " bypassed (no plugin context)";
// --- open event ---
if (event_name == "open") {
const char* path_cstr = nullptr;
const char* mode_cstr = nullptr;
int flags = 0;
// open(path, mode, flags) — path may be str, bytes, or int fd
if (!PyArg_ParseTuple(args, "s|si", &path_cstr, &mode_cstr, &flags)) {
PyErr_Clear(); // couldn't parse; allow
return 0; return 0;
} }
std::string path_str(path_cstr ? path_cstr : ""); // the open function can take in different flags that determine if it is a read or readwrite.
std::string mode_str(mode_cstr ? mode_cstr : "r"); const AuditEventCategory event_type =
event_name == "open" ? PluginAuditDetail::open_category(args) : event_category(event_name);
if (event_type == AuditEventCategory::None)
return 0;
AuditDecision decision = mgr->check_open(path_str, mode_str); const bool fs_category = is_fs_category(event_type);
if (!decision.allowed) const std::vector<std::string> targets = PluginAuditDetail::audit_targets(event_name, event_type, args);
return report_denied(*mgr, event_name, path_str, decision);
// A denied path (secrets, certificates, config files -- see is_denied_path) is an
// unconditional block: checked before the ancestor-cascade check below, so a cascade
// approval recorded for an unrelated action can never launder access to one, and before
// the allowed-root shortcut further down, so an allowed root (e.g. the read-only resources
// folder) cannot make a denied path underneath it reachable.
if (fs_category) {
for (const auto& target : targets) {
if (mgr->is_denied_path(boost::filesystem::path(target)))
return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"});
}
}
PluginDescriptor plugin_descriptor;
PluginManager::instance().try_get_plugin_descriptor(mgr->current_plugin(), plugin_descriptor);
// Some plugins call other audited events, e.g. urlib.request will call socket.connect. So this is so that if urlib.request
// was already approved, socket.connect won't trigger another permission dialog.
const std::vector<std::string> call_site_ids =
PluginAuditDetail::call_site_identities(plugin_descriptor.plugin_root);
if (mgr->has_approved_ancestor(mgr->current_plugin(), call_site_ids))
return 0;
// A filesystem target that resolves entirely inside a pre-determined allowed root -- the
// plugin system's own data_dir() tree (which holds each plugin's storage folder and the
// installed/system profile cache), the read-only bundled resources folder, or a per-call
// scoped root such as the current G-code folder -- is part of the plugin system's normal
// workflow and does not need a prompt.
if (fs_category && !targets.empty()) {
const bool is_write = event_type != AuditEventCategory::FsRead;
const bool all_inside_allowed_root =
std::all_of(targets.begin(), targets.end(), [&](const std::string& target) {
return mgr->check_path_access(boost::filesystem::path(target), is_write).allowed;
});
if (all_inside_allowed_root)
return 0; return 0;
} }
// --- os.rename event (raised by os.rename and os.replace) --- PluginInstallState state;
if (event_name == "os.rename") { const bool have_install_state = PluginManager::instance().get_install_state(mgr->current_plugin(), state);
const char* src_cstr = nullptr; if (!have_install_state) {
const char* dst_cstr = nullptr; BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr->current_plugin();
PyObject* src_dir_fd = nullptr;
PyObject* dst_dir_fd = nullptr;
// os.rename(src, dst, src_dir_fd, dst_dir_fd) — paths may be str, bytes, or int fd.
// The dir_fd arguments are unused, but must be accepted for the tuple to parse.
if (!PyArg_ParseTuple(args, "ss|OO", &src_cstr, &dst_cstr, &src_dir_fd, &dst_dir_fd)) {
PyErr_Clear(); // couldn't parse; allow
return 0;
} }
// A rename writes at both ends, so either end being denied blocks the call. const std::string plugin_name = state.plugin_name.empty() ? mgr->current_plugin() : state.plugin_name;
for (const char* path_cstr : {src_cstr, dst_cstr}) {
std::string path_str(path_cstr ? path_cstr : "");
AuditDecision decision = mgr->check_path_access(path_str, /* is_write */ true);
if (!decision.allowed)
return report_denied(*mgr, event_name, path_str, decision);
}
return 0;
}
// --- os.remove event (raised by os.remove and os.unlink) --- std::vector<std::string>* permission_list = PluginAuditDetail::permission_list_for(event_type, state.permissions);
if (event_name == "os.remove") {
const char* path_cstr = nullptr;
PyObject* dir_fd = nullptr;
// os.remove(path, dir_fd) — path may be str, bytes, or int fd return PluginAuditDetail::decide_audited_event(*mgr, state, mgr->current_plugin(), plugin_name, event_name,
if (!PyArg_ParseTuple(args, "s|O", &path_cstr, &dir_fd)) { event_type, targets, permission_list, call_site_ids);
PyErr_Clear(); // couldn't parse; allow
return 0;
}
std::string path_str(path_cstr ? path_cstr : "");
AuditDecision decision = mgr->check_path_access(path_str, /* is_write */ true);
if (!decision.allowed)
return report_denied(*mgr, event_name, path_str, decision);
return 0;
}
// Unknown event — allow by default
return 0;
} }
void PluginAuditManager::install_hook() void PluginAuditManager::install_hook()
{ {
if (PySys_AddAuditHook(audit_hook, this) < 0) { // data_dir() is the primary globally-allowed root during plugin execution: read+write. It
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook"; // covers the plugin system's own workflow needs -- each plugin's storage folder
return; // (data_dir()/orca_plugins) and the installed/system profile cache (data_dir()/system) --
} // without a separate, narrower grant for either (G-code plugins additionally get the temp
BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully"; // G-code folder via a scoped root, see SlicingPipelinePluginCapabilityTrampoline).
// data_dir() is the only globally-allowed root during enforced plugin execution.
// The executable directory and resources directory are intentionally NOT allowed
// here: plugins must not write outside data_dir() (G-code plugins additionally get
// the temp G-code folder via a scoped root). Reads remain permissive in Loading mode.
add_global_allowed_root(data_dir()); add_global_allowed_root(data_dir());
// resources_dir() holds the app's bundled, shared assets (installed system profiles, the
// bundled TLS client cert, web assets). Plugins may read from it -- e.g. inspecting bundled
// profiles -- but must never write into the shared, potentially multi-user app install, so
// it is granted read-only. The bundled cert itself stays unreachable regardless, via the
// "cert" denied-path keyword seeded below.
add_global_allowed_root(resources_dir(), /*allow_write=*/false);
// The user's app config and cloud credentials live directly inside data_dir(), so the // The user's app config and cloud credentials live directly inside data_dir(), so the
// root just granted would otherwise expose them to any plugin. Deny them by name. // root just granted would otherwise expose them to any plugin. Deny them by name.
// //
@@ -430,6 +974,19 @@ void PluginAuditManager::install_hook()
// (see its comment for why all four config names are denied); the tests seed from it too. // (see its comment for why all four config names are denied); the tests seed from it too.
for (const auto& name : default_denied_filenames()) for (const auto& name : default_denied_filenames())
add_denied_filename(name); add_denied_filename(name);
// Categorical denies on top of the exact-name list above: no path a plugin can reach may
// contain a "secret", "cert"(ificate), or "conf"(ig) path component, regardless of which
// allowed root it happens to sit inside. default_denied_path_keywords() is the single
// source of this list; the tests seed from it too.
for (const auto& keyword : default_denied_path_keywords())
add_denied_path_keyword(keyword);
if (PySys_AddAuditHook(audit_hook, this) < 0) {
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install CPython audit hook";
return;
}
BOOST_LOG_TRIVIAL(info) << "[AUDIT] CPython audit hook installed successfully";
} }
} // namespace Slic3r } // namespace Slic3r
+83 -29
View File
@@ -2,8 +2,11 @@
#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>
@@ -18,10 +21,31 @@ 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,
@@ -49,12 +73,15 @@ public:
void clear_current_capability(); void clear_current_capability();
// --- allowed-roots registry --- // --- allowed-roots registry ---
void add_global_allowed_root(const boost::filesystem::path& root); // allow_write = false registers a read-only root: a read-shaped event inside it is allowed,
void add_scoped_allowed_root(const boost::filesystem::path& root); // but a write/create/delete-shaped event is not, so it falls through to the normal
// 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 audit mode or // Filenames a plugin may never touch, in any directory, regardless of the enclosing allowed
// enclosing allowed root. A candidate is denied when its filename starts with a // 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
@@ -73,33 +100,60 @@ 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;
// --- enforcement mode --- // --- denied-path-keyword registry ---
enum class AuditMode { // Keywords that categorically deny a path if ANY of its components (directory or file
// Import/loading phase: allow reads anywhere, only block writes // name), not just the base name, contains one case-insensitively -- e.g. a "secrets"
// outside allowed roots. Python needs to read stdlib modules // subfolder, a "certificates" folder, or a "conf"/"config" file anywhere the plugin can
// during import and those are not inside plugin directories. // otherwise reach, including inside an allowed root. This is intentionally broader and
Loading, // fuzzier than the exact-name is_denied_filename registry: it exists to categorically rule
// 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);
// Execution phase: block both reads and writes outside allowed // The list install_hook() seeds into the keyword registry. Exposed so tests seed the exact
// roots, plus subprocess/socket/ctypes. // same set without a live interpreter.
Enforcing, static std::vector<std::string> default_denied_path_keywords();
};
void set_audit_mode(AuditMode mode); // True when any component of candidate's (canonicalized) path contains a registered
AuditMode audit_mode() const; // keyword, case-insensitively.
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. The deny list is consulted above the // Shared core for every audited filesystem event. The deny checks are consulted above the
// Loading-mode read exemption and above the allowed roots, so a denied filename is // allowed roots, so a denied path is blocked even when it sits inside an allowed root (e.g.
// blocked even though every scope currently runs in Loading and the files in question // data_dir(), which is a global allowed root).
// 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:
@@ -111,15 +165,17 @@ private:
static thread_local std::string m_current_plugin_key; static thread_local std::string m_current_plugin_key;
static thread_local std::string m_current_capability_name; static thread_local std::string m_current_capability_name;
static thread_local AuditMode m_audit_mode; static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots;
static thread_local std::vector<boost::filesystem::path> m_scoped_allowed_roots; static thread_local bool m_audit_denial_pending;
static thread_local bool m_has_last_violation; static thread_local bool m_has_last_violation;
static thread_local AuditViolation m_last_violation; static thread_local AuditViolation m_last_violation;
// mutable: is_denied_filename() is a const query that must lock. // 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<boost::filesystem::path> m_global_allowed_roots; std::vector<AllowedRoot> 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
@@ -130,8 +186,7 @@ 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();
@@ -141,8 +196,7 @@ public:
private: private:
std::string m_previous_id; std::string m_previous_id;
std::string m_previous_capability; std::string m_previous_capability;
PluginAuditManager::AuditMode m_previous_mode; std::vector<AllowedRoot> m_previous_scoped_roots;
std::vector<boost::filesystem::path> m_previous_scoped_roots;
}; };
} // namespace Slic3r } // namespace Slic3r
+38 -4
View File
@@ -784,6 +784,23 @@ 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>();
@@ -819,6 +836,14 @@ 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}});
@@ -836,6 +861,9 @@ 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
@@ -852,10 +880,16 @@ 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". PluginManager writes the real // sidecar is (re)initialized to "auto-load, nothing disabled". This intentionally resets
// per-capability flags once the package is loaded, via the (dir, entry, enabled, capabilities) // permissions on a fresh install/reinstall. PluginManager writes the real per-capability flags
// overload. // while preserving permissions once the package is loaded, via the overload above.
return write_install_state(plugin_dir, entry, true, {}); PluginInstallState state;
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)
+12
View File
@@ -82,11 +82,23 @@ 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
}; };
+1 -1
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, bridge_error); auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, descriptor.plugin_key, 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;
+68
View File
@@ -21,6 +21,7 @@
#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>
@@ -622,6 +623,20 @@ 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
@@ -910,6 +925,8 @@ 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;
} }
@@ -1135,6 +1152,51 @@ 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)
@@ -1354,6 +1416,11 @@ 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);
@@ -1741,6 +1808,7 @@ 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;
} }
+5
View File
@@ -159,6 +159,8 @@ 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();
@@ -255,6 +257,9 @@ 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);
+15 -12
View File
@@ -26,25 +26,30 @@
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(mode) \ #define ORCA_PY_AUDIT_SCOPE() \
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(), mode) _orca_audit_scope.emplace(_orca_audit_key, this->name())
#define ORCA_PY_OVERRIDE_AUDITED(mode, audit_setup, override_macro, ret, base, name, ...) \ #define ORCA_PY_OVERRIDE_AUDITED(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(mode); \ ORCA_PY_AUDIT_SCOPE(); \
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__)); \
@@ -58,7 +63,7 @@ public:
std::string get_name() const override std::string get_name() const override
{ {
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, Base, get_name); ORCA_PY_OVERRIDE_AUDITED([] {}, 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
@@ -66,7 +71,6 @@ 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,
@@ -77,7 +81,6 @@ 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,
@@ -94,7 +97,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(::Slic3r::PluginAuditManager::AuditMode::Loading); ORCA_PY_AUDIT_SCOPE();
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");
@@ -117,17 +120,17 @@ public:
void on_load() override void on_load() override
{ {
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_load); ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_load);
} }
void on_unload() override void on_unload() override
{ {
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_unload); ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_unload);
} }
void on_cancelled() override void on_cancelled() override
{ {
ORCA_PY_OVERRIDE_AUDITED(::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled); ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled);
} }
}; };
@@ -139,7 +142,7 @@ public:
PluginCapabilityType get_type() const override PluginCapabilityType get_type() const override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface, [] {}, PYBIND11_OVERRIDE, PluginCapabilityType, PluginCapabilityInterface,
get_type); get_type);
} }
}; };
+6 -4
View File
@@ -87,6 +87,12 @@ 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
@@ -622,10 +628,6 @@ 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
+75 -16
View File
@@ -1,5 +1,6 @@
#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>
@@ -40,6 +41,7 @@ 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
@@ -77,6 +79,8 @@ 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
@@ -101,34 +105,38 @@ 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& plugin_key, std::string& error) std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(const std::string& capture_key,
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 " << plugin_key; BOOST_LOG_TRIVIAL(info) << "Finalizing Python plugin capture for key " << capture_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 = [&plugin_key]() { auto clear_active_key = [&capture_key]() {
if (g_active_plugin_key == plugin_key) if (g_active_plugin_key == capture_key)
g_active_plugin_key.clear(); g_active_plugin_key.clear();
}; };
auto discard_pending_for_key = [&plugin_key]() { auto discard_pending_for_key = [&capture_key]() {
std::lock_guard<std::mutex> lock(g_registry_mutex); std::lock_guard<std::mutex> lock(g_registry_mutex);
g_pending_capabilities.erase(plugin_key); g_pending_capabilities.erase(capture_key);
g_pending_package.erase(plugin_key); g_pending_package.erase(capture_key);
g_pending_fs_read_permissions.erase(capture_key);
}; };
try { try {
@@ -138,7 +146,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(plugin_key); auto it = g_pending_package.find(capture_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);
@@ -157,6 +165,33 @@ 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();
@@ -180,7 +215,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(plugin_key); auto it = g_pending_capabilities.find(capture_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);
@@ -189,10 +224,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 == plugin_key) if (g_active_plugin_key == capture_key)
g_active_plugin_key.clear(); g_active_plugin_key.clear();
BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << plugin_key; BOOST_LOG_TRIVIAL(info) << "Collected " << classes.size() << " registered capability class(es) for key " << capture_key;
std::vector<CapturedCapability> capabilities; std::vector<CapturedCapability> capabilities;
capabilities.reserve(classes.size()); capabilities.reserve(classes.size());
@@ -204,7 +239,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 " << plugin_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed type check for key " << capture_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -212,7 +247,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 " << plugin_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture failed cast for key " << capture_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -226,7 +261,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 " << plugin_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture rejected capability for key " << capture_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -246,12 +281,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 " << plugin_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised Python exception for key " << capture_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 " << plugin_key BOOST_LOG_TRIVIAL(error) << "Python plugin capture raised exception for key " << capture_key
<< " error=" << error; << " error=" << error;
return {}; return {};
} }
@@ -279,6 +314,7 @@ 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)
@@ -305,6 +341,7 @@ 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;
} }
@@ -313,6 +350,7 @@ 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();
} }
@@ -450,6 +488,27 @@ 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");
+1 -1
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& plugin_key, std::string& error); const std::string& capture_key, 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);
@@ -16,7 +16,6 @@ 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,
@@ -27,7 +26,6 @@ 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,
@@ -42,7 +40,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(::Slic3r::PluginAuditManager::AuditMode::Loading); ORCA_PY_AUDIT_SCOPE();
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)
@@ -16,206 +16,206 @@ public:
AgentInfo get_agent_info() override AgentInfo get_agent_info() override
{ {
ORCA_PY_OVERRIDE_AUDITED( ORCA_PY_OVERRIDE_AUDITED(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, AgentInfo, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, connect_printer, dev_id, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, disconnect_printer); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message, dev_id, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, send_message_to_printer, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, start_discovery, start, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind_detect, dev_ip, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_local_print, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, FilamentSyncMode, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, bool, PrinterAgentPluginCapability, fetch_filament_info, dev_id); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, check_cert); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, void, PrinterAgentPluginCapability, install_device_cert, dev_id, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, ping_bind, ping_code); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, bind, dev_ip, dev_id, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, unbind, dev_id); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_print, params, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, start_sdcard_print, params, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, get_hms_snapshot, dev_id, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_server_callback, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_ssdp_msg_fn, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_printer_connected_fn, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_subscribe_failure_fn, [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_message_fn, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_user_message_fn, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_connect_fn, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_on_local_message_fn, fn); [] {}, 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(
::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, int, PrinterAgentPluginCapability, set_queue_on_main_fn, fn); [] {}, 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(::Slic3r::PluginAuditManager::AuditMode::Loading); ORCA_PY_AUDIT_SCOPE();
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this); ::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
::Slic3r::PythonGILState gil; ::Slic3r::PythonGILState gil;
if (!gil) if (!gil)
@@ -13,7 +13,6 @@ 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,
@@ -10,7 +10,6 @@ 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
+29 -7
View File
@@ -30,14 +30,16 @@ if (APPLE)
target_link_libraries(test_common INTERFACE "-liconv -framework IOKit" "-framework CoreFoundation" -lc++) target_link_libraries(test_common INTERFACE "-liconv -framework IOKit" "-framework CoreFoundation" -lc++)
endif() endif()
# Copies runtime DLLs next to each test executable. Handles both single-config # Copies runtime shared libraries next to each test executable. Handles both
# generators (CMAKE_BUILD_TYPE set) and multi-config generators (Ninja # single-config generators (CMAKE_BUILD_TYPE set) and multi-config generators
# Multi-Config, Visual Studio) where CMAKE_BUILD_TYPE is empty and DLLs must # (Ninja Multi-Config, Visual Studio) where CMAKE_BUILD_TYPE is empty and DLLs
# land in every per-config output directory. # must land in every per-config output directory. On Windows the loader finds
# DLLs in the executable's directory; the Linux branch below does the same for
# the deps-built FFmpeg libraries and adds an $ORIGIN rpath, since the ELF
# loader does not search the executable's directory and the CI unit-test runner
# only receives the tests artifact (no deps install).
function(orcaslicer_copy_test_dlls) function(orcaslicer_copy_test_dlls)
if (NOT WIN32) if (WIN32)
return()
endif()
set(_configs ${CMAKE_CONFIGURATION_TYPES}) set(_configs ${CMAKE_CONFIGURATION_TYPES})
if (NOT _configs) if (NOT _configs)
set(_configs "${CMAKE_BUILD_TYPE}") set(_configs "${CMAKE_BUILD_TYPE}")
@@ -49,6 +51,26 @@ function(orcaslicer_copy_test_dlls)
orcaslicer_copy_dlls(COPY_DLLS "${_cfg}" "" _unused_dlls) orcaslicer_copy_dlls(COPY_DLLS "${_cfg}" "" _unused_dlls)
endif() endif()
endforeach() endforeach()
elseif (UNIX AND NOT APPLE)
# Only test executables that link libslic3r_gui pull in the FFmpeg
# shared libraries (src/slic3r/CMakeLists.txt links PkgConfig::LIBAV
# into it). Copy them next to the executable and give it an $ORIGIN
# rpath so the loader finds them when the tests run on the CI unit-test
# runner, which only receives this build/tests tree.
get_target_property(_linked_libs ${_TEST_NAME}_tests LINK_LIBRARIES)
if (NOT "libslic3r_gui" IN_LIST _linked_libs)
return()
endif()
set_property(TARGET ${_TEST_NAME}_tests PROPERTY BUILD_RPATH "$ORIGIN")
set(_configs ${CMAKE_CONFIGURATION_TYPES})
if (NOT _configs)
set(_configs "${CMAKE_BUILD_TYPE}")
endif()
foreach(_cfg IN LISTS _configs)
orcaslicer_copy_sos(${_TEST_NAME}_tests "${_cfg}" "" _unused_sos)
endforeach()
endif()
endfunction() endfunction()
# Register Catch2 tags as CTest labels so `ctest -L`/`-LE` can filter by tag. # Register Catch2 tags as CTest labels so `ctest -L`/`-LE` can filter by tag.
+20
View File
@@ -34,4 +34,24 @@ 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
+159 -8
View File
@@ -27,6 +27,16 @@ 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]")
@@ -81,7 +91,7 @@ TEST_CASE("Plugin audit denies app config and token filenames anywhere", "[audit
} }
} }
TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption", "[audit]") TEST_CASE("Plugin audit deny beats allowed roots", "[audit]")
{ {
ScopedDataDir data_dir_guard("plugin-audit-deny"); ScopedDataDir data_dir_guard("plugin-audit-deny");
seed_denied_names(); seed_denied_names();
@@ -91,8 +101,8 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption"
// 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 in Loading mode, which every scope runs in. // Enter a plugin context. The deny must hold even inside a globally allowed root.
ScopedPluginAuditContext ctx("test_plugin", "", PluginAuditManager::AuditMode::Loading); ScopedPluginAuditContext ctx("test_plugin", "");
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;
@@ -103,6 +113,13 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption"
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");
@@ -110,16 +127,14 @@ TEST_CASE("Plugin audit deny beats allowed roots and the Loading read exemption"
CHECK(decision.reason == "denied filename"); CHECK(decision.reason == "denied filename");
} }
SECTION("reading the app config is blocked even though Loading exempts reads") SECTION("reading the app config is blocked despite the allowed root")
{ {
// 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 in Loading mode") SECTION("reading the cloud refresh token is blocked")
{ {
AuditDecision decision = mgr.check_open(token.string(), "r"); AuditDecision decision = mgr.check_open(token.string(), "r");
CHECK_FALSE(decision.allowed); CHECK_FALSE(decision.allowed);
@@ -150,7 +165,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", "", PluginAuditManager::AuditMode::Loading); ScopedPluginAuditContext ctx("test_plugin", "");
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")
@@ -185,3 +200,139 @@ 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);
}
}
+16
View File
@@ -112,6 +112,22 @@ 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