mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-21 06:21:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77ca086f67 | ||
|
|
3dff91b9ff | ||
|
|
8f10d48a76 | ||
|
|
0aa1b5fb69 |
No files matched your search
@@ -123,6 +123,7 @@ option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0)
|
||||
|
||||
# Python stubgen module
|
||||
option(ORCA_BUILD_PYTHON_STUBGEN_MODULE "Build importable Python module for pybind11-stubgen" ON)
|
||||
option(ORCA_BUILD_PLUGIN_PERMISSION_PROBE "Build the headless Python plugin permission probe (developer tool)" OFF)
|
||||
set(ORCA_BUNDLED_UV_EXECUTABLE "" CACHE FILEPATH "Path to a uv executable to bundle for Python package installation. Leave empty to auto-download.")
|
||||
|
||||
# Auto-download uv if not provided by the user.
|
||||
|
||||
Vendored
+2
-6
@@ -59,13 +59,9 @@ else ()
|
||||
set(_cross_compile_arg --host=${TOOLCHAIN_PREFIX})
|
||||
endif ()
|
||||
|
||||
# 6.3.0 is the first release whose arm64 assembly avoids x18, which macOS
|
||||
# reserves and zeroes on every return to user space; 6.2.1 crashes on Apple
|
||||
# silicon whenever CGAL falls back to exact arithmetic.
|
||||
ExternalProject_Add(dep_GMP
|
||||
URL https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
|
||||
https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.3.0/gmp-6.3.0.tar.xz
|
||||
URL_HASH SHA256=a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
|
||||
URL https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
|
||||
URL_HASH SHA256=eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
|
||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/GMP
|
||||
PATCH_COMMAND git apply ${GMP_DIRECTORY_FLAG} --verbose ${CMAKE_CURRENT_LIST_DIR}/0001-GMP_GCC15.patch
|
||||
BUILD_IN_SOURCE ON
|
||||
|
||||
@@ -366,6 +366,7 @@ src/slic3r/Utils/MoonrakerPrinterAgent.cpp
|
||||
src/slic3r/Utils/BBLNetworkPlugin.cpp
|
||||
src/slic3r/Utils/OrcaCloudServiceAgent.cpp
|
||||
src/slic3r/plugin/PluginAuditManager.cpp
|
||||
src/slic3r/plugin/PluginAuditManagerGui.cpp
|
||||
src/slic3r/plugin/PluginConfig.cpp
|
||||
src/slic3r/plugin/PluginResolver.cpp
|
||||
src/slic3r/plugin/CloudPluginService.cpp
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Flashforge",
|
||||
"url": "",
|
||||
"version": "02.04.00.12",
|
||||
"version": "02.04.00.11",
|
||||
"force_update": "0",
|
||||
"description": "Flashforge configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"compatible_printers": [
|
||||
"Flashforge Adventurer 5M 0.4 Nozzle"
|
||||
],
|
||||
"exclude_object_tolerance": "0.2",
|
||||
"filename_format": "{input_filename_base}_{filament_type[initial_tool]}_{print_time}.gcode",
|
||||
"post_process": "",
|
||||
"only_one_wall_top": "0",
|
||||
|
||||
-1
@@ -8,7 +8,6 @@
|
||||
"compatible_printers": [
|
||||
"Flashforge Adventurer 5M Pro 0.4 Nozzle"
|
||||
],
|
||||
"exclude_object_tolerance": "0.2",
|
||||
"filename_format": "{input_filename_base}_{filament_type[0]}_{print_time}.gcode",
|
||||
"post_process": "",
|
||||
"only_one_wall_top": "0",
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"compatible_printers": [
|
||||
"Flashforge Adventurer 5M 0.6 Nozzle"
|
||||
],
|
||||
"exclude_object_tolerance": "0.2",
|
||||
"filename_format": "{input_filename_base}_{filament_type[initial_tool]}_{print_time}.gcode",
|
||||
"post_process": "",
|
||||
"only_one_wall_top": "0",
|
||||
|
||||
-1
@@ -8,7 +8,6 @@
|
||||
"compatible_printers": [
|
||||
"Flashforge Adventurer 5M Pro 0.6 Nozzle"
|
||||
],
|
||||
"exclude_object_tolerance": "0.2",
|
||||
"filename_format": "{input_filename_base}_{filament_type[initial_tool]}_{print_time}.gcode",
|
||||
"post_process": "",
|
||||
"only_one_wall_top": "0",
|
||||
|
||||
@@ -226,10 +226,10 @@ modules:
|
||||
sha256: f353e7b99bd0cbfc27ac9082de51acf32a8bc0b3e21ff9661ecca6f205ec1d81
|
||||
dest: external-packages/OpenVDB
|
||||
|
||||
# GMP 6.3.0
|
||||
# GMP 6.2.1
|
||||
- type: file
|
||||
url: https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz
|
||||
sha256: a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898
|
||||
url: https://github.com/SoftFever/OrcaSlicer_deps/releases/download/gmp-6.2.1/gmp-6.2.1.tar.bz2
|
||||
sha256: eae9326beb4158c386e39a356818031bd28f3124cf915f8c5b1dc4c7a36b4d7c
|
||||
dest: external-packages/GMP
|
||||
|
||||
# MPFR 4.2.2
|
||||
|
||||
@@ -23,7 +23,7 @@ if %FULL_MODE%==1 (
|
||||
call :prepareGettextList "%list_file%" "%filtered_list%" "%missing_list%"
|
||||
if "!has_sources!"=="1" (
|
||||
if not exist "%generated_i18n%" mkdir "%generated_i18n%"
|
||||
.\tools\xgettext.exe --keyword=L --keyword=_L --keyword=_u8L --keyword=L_CONTEXT:1,2c --keyword=_L_CONTEXT:1,2c --keyword=_u8L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost --no-wrap -f "%filtered_list%" -o "%generated_pot%"
|
||||
.\tools\xgettext.exe --keyword=L --keyword=_L --keyword=_u8L --keyword=PLUGIN_U8L --keyword=L_CONTEXT:1,2c --keyword=_L_CONTEXT:1,2c --keyword=_u8L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost --no-wrap -f "%filtered_list%" -o "%generated_pot%"
|
||||
if errorlevel 1 (
|
||||
set "script_exit_code=1"
|
||||
) else (
|
||||
|
||||
@@ -85,7 +85,7 @@ if $FULL_MODE; then
|
||||
generated_pot_file="${generated_i18n_dir}/OrcaSlicer.pot"
|
||||
|
||||
mkdir -p "$generated_i18n_dir"
|
||||
xgettext --keyword=L --keyword=_L --keyword=_u8L --keyword=L_CONTEXT:1,2c --keyword=_L_CONTEXT:1,2c --keyword=_u8L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost --no-wrap -f "$filtered_list" -o "$generated_pot_file"
|
||||
xgettext --keyword=L --keyword=_L --keyword=_u8L --keyword=PLUGIN_U8L --keyword=L_CONTEXT:1,2c --keyword=_L_CONTEXT:1,2c --keyword=_u8L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost --no-wrap -f "$filtered_list" -o "$generated_pot_file"
|
||||
python3 scripts/HintsToPot.py ./resources "$generated_i18n_dir"
|
||||
|
||||
if [ -f "$pot_file" ] && files_equal_ignoring_pot_date "$pot_file" "$generated_pot_file"; then
|
||||
|
||||
@@ -122,8 +122,8 @@ void update_arrange_params(ArrangeParams& params, const DynamicPrintConfig* prin
|
||||
|
||||
void update_selected_items_inflation(ArrangePolygons& selected, const DynamicPrintConfig* print_cfg, ArrangeParams& params) {
|
||||
// do not inflate brim_width. Objects are allowed to have overlapped brim.
|
||||
// A brim reaching the printable area's edge is fine, so measure against the full bed.
|
||||
BoundingBox bedbb = Polygon(get_bed_shape(*print_cfg)).bounding_box();
|
||||
Points bedpts = get_shrink_bedpts(print_cfg, params);
|
||||
BoundingBox bedbb = Polygon(bedpts).bounding_box();
|
||||
// set obj distance for auto seq_print
|
||||
if (params.is_seq_print) {
|
||||
bool all_objects_are_short = std::all_of(selected.begin(), selected.end(), [&](ArrangePolygon& ap) { return ap.height < params.nozzle_height; });
|
||||
@@ -144,30 +144,14 @@ void update_selected_items_inflation(ArrangePolygons& selected, const DynamicPri
|
||||
// 3. otherwise, use each object's own brim width
|
||||
ap.inflation = params.min_obj_distance != 0 ? params.min_obj_distance / 2 :
|
||||
plate_has_tree_support ? scaled(brim_max / 2) : scaled(ap.brim_width);
|
||||
if (params.min_obj_distance != 0)
|
||||
return; // an explicit object distance is the user's call, not ours to shrink
|
||||
// An item cannot be larger than the printable area, inflation included. Inflating
|
||||
// an almost bed filling item would leave it too big to ever be placed.
|
||||
BoundingBox apbb = ap.poly.contour.bounding_box();
|
||||
auto diffx = bedbb.size().x() - apbb.size().x();
|
||||
auto diffy = bedbb.size().y() - apbb.size().y();
|
||||
ap.inflation = std::max<coord_t>(0, std::min({ ap.inflation, diffx / 2, diffy / 2 }));
|
||||
});
|
||||
|
||||
// Give the bed margin, which the arrange bed keeps from the printable area, back
|
||||
// where the largest item needs it. The skirt keeps its room: pushing a skirt off
|
||||
// the bed is worse than saying the item does not fit.
|
||||
if (!selected.empty()) {
|
||||
coord_t max_width = 0, max_height = 0;
|
||||
for (const ArrangePolygon &ap : selected) {
|
||||
BoundingBox apbb = ap.poly.contour.bounding_box();
|
||||
max_width = std::max(max_width, apbb.size().x() + 2 * ap.inflation);
|
||||
max_height = std::max(max_height, apbb.size().y() + 2 * ap.inflation);
|
||||
BoundingBox apbb = ap.poly.contour.bounding_box();
|
||||
auto diffx = bedbb.size().x() - apbb.size().x() - 5;
|
||||
auto diffy = bedbb.size().y() - apbb.size().y() - 5;
|
||||
if (diffx > 0 && diffy > 0) {
|
||||
auto min_diff = std::min(diffx, diffy);
|
||||
ap.inflation = std::min(min_diff / 2, ap.inflation);
|
||||
}
|
||||
auto margin = [](coord_t bed, coord_t item) { return std::max(0., unscale<double>(bed - item) / 2.); };
|
||||
params.bed_shrink_x = std::min<double>(params.bed_shrink_x, std::max<double>(params.brim_skirt_distance, margin(bedbb.size().x(), max_width)));
|
||||
params.bed_shrink_y = std::min<double>(params.bed_shrink_y, std::max<double>(params.brim_skirt_distance, margin(bedbb.size().y(), max_height)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void update_unselected_items_inflation(ArrangePolygons& unselected, const DynamicPrintConfig* print_cfg, const ArrangeParams& params)
|
||||
|
||||
@@ -2914,7 +2914,7 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin
|
||||
|
||||
const float delta = float(scale_(t));
|
||||
// Arc tolerance of 1/500 of the offset, Clipper2's default.
|
||||
for (const Polygon &ring : offset(polylines, delta, jtMiter, 0.002 * delta, etOpenRound))
|
||||
for (const Polygon &ring : offset(polylines, delta, jtRound, 0.002 * delta, etOpenRound))
|
||||
if (ring.size() >= 3)
|
||||
all_polylines.emplace_back(ring.split_at_first_point());
|
||||
}
|
||||
|
||||
+14
-14
@@ -3979,22 +3979,22 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
this->placeholder_parser().set("hold_chamber_temp_for_flat_print", new ConfigOptionBool(hold_chamber_temp_for_flat_print));
|
||||
}
|
||||
|
||||
this->placeholder_parser().set_deferred("print_time_total_sec", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder));
|
||||
this->placeholder_parser().set_deferred("print_time_day", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder));
|
||||
this->placeholder_parser().set_deferred("print_time_hour", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder));
|
||||
this->placeholder_parser().set_deferred("print_time_minute", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder));
|
||||
this->placeholder_parser().set_deferred("print_time_sec", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder));
|
||||
this->placeholder_parser().set_deferred("used_filament_length", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder));
|
||||
this->placeholder_parser().set("print_time_total_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_day", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_hour", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
|
||||
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
|
||||
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
|
||||
// The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too.
|
||||
// The running extruded_* totals are left alone; profiles use them as numbers.
|
||||
this->placeholder_parser().set_deferred("used_filament", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder));
|
||||
this->placeholder_parser().set_deferred("print_time", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder));
|
||||
this->placeholder_parser().set_deferred("normal_print_time", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder));
|
||||
this->placeholder_parser().set_deferred("silent_print_time", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder));
|
||||
this->placeholder_parser().set_deferred("total_weight", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder));
|
||||
this->placeholder_parser().set_deferred("total_cost", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder));
|
||||
this->placeholder_parser().set_deferred("total_wipe_tower_cost", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder));
|
||||
this->placeholder_parser().set_deferred("total_wipe_tower_filament", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder));
|
||||
this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
|
||||
this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
|
||||
this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
|
||||
this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder)));
|
||||
this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder)));
|
||||
this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder)));
|
||||
this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder)));
|
||||
this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder)));
|
||||
|
||||
// Sync variant-mapped params into placeholder_parser before processing start gcode
|
||||
update_placeholder_parser_with_variant_params();
|
||||
|
||||
@@ -213,29 +213,13 @@ int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transfo
|
||||
return -1;
|
||||
}
|
||||
|
||||
Transform3d lay_on_face_rotation(const Vec3d &normal, const Transform3d &instance_matrix_no_offset)
|
||||
{
|
||||
// Normal in instance coordinates: a normal transforms by the inverse transpose.
|
||||
const Vec3d tnormal = (instance_matrix_no_offset.matrix().block(0, 0, 3, 3).inverse().transpose() * normal).normalized();
|
||||
const Vec3d down = -Vec3d::UnitZ();
|
||||
|
||||
// A face already pointing straight up needs a half turn, and every horizontal axis does
|
||||
// that equally well. setFromTwoVectors() resolves the choice through an SVD whose null
|
||||
// space is the whole XY plane, so the axis follows rounding noise and the object lands at
|
||||
// an arbitrary angle in the plate plane. Turn it over about X instead, which keeps the
|
||||
// footprint the object already had.
|
||||
if (tnormal.dot(down) < -1. + 1e-9)
|
||||
return Transform3d(Eigen::AngleAxisd(PI, Vec3d::UnitX()));
|
||||
|
||||
return Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, down));
|
||||
}
|
||||
|
||||
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal)
|
||||
{
|
||||
ModelInstance &instance = *object.instances[instance_idx];
|
||||
const Geometry::Transformation &trafo = instance.get_transformation();
|
||||
// Same rotation as Selection::flattening_rotate(), which the gizmo uses.
|
||||
const Transform3d rotation = lay_on_face_rotation(normal, trafo.get_matrix_no_offset());
|
||||
// Same rotation as Selection::flattening_rotate(): turn the transformed normal to point down.
|
||||
const Vec3d tnormal = trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal;
|
||||
const Transform3d rotation = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()));
|
||||
instance.set_transformation(Geometry::Transformation(trafo.get_offset_matrix() * rotation * trafo.get_matrix_no_offset()));
|
||||
// Drop this instance only: ensure_on_bed() skips instances without auto_drop and measures the first instance.
|
||||
object.translate_instance(instance_idx, -object.instance_bounding_box(instance_idx).min.z() * Vec3d::UnitZ());
|
||||
|
||||
@@ -46,10 +46,4 @@ int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transfo
|
||||
// the gizmo applies, then drops the instance so its lowest point is at z = 0.
|
||||
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal);
|
||||
|
||||
// The rotation that turns `normal` (object coordinates) to point down, given the instance's
|
||||
// rotation, scale and mirror. A face that already points straight up is turned over about X:
|
||||
// the axis setFromTwoVectors() picks there is arbitrary, and it lands the object at a random
|
||||
// angle in the plate plane.
|
||||
Transform3d lay_on_face_rotation(const Vec3d &normal, const Transform3d &instance_matrix_no_offset);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -243,7 +243,7 @@ namespace client
|
||||
struct expr
|
||||
{
|
||||
expr() {}
|
||||
expr(const expr &rhs) : m_type(rhs.type()), it_range(rhs.it_range), m_deferred(rhs.m_deferred)
|
||||
expr(const expr &rhs) : m_type(rhs.type()), it_range(rhs.it_range)
|
||||
{ if (rhs.type() == TYPE_STRING) m_data.s = new std::string(*rhs.m_data.s); else m_data.set(rhs.m_data); }
|
||||
expr(expr &&rhs) : expr(std::move(rhs), rhs.it_range.begin(), rhs.it_range.end()) {}
|
||||
|
||||
@@ -258,16 +258,13 @@ namespace client
|
||||
explicit expr(std::string &&s) : m_type(TYPE_STRING) { m_data.s = new std::string(std::move(s)); }
|
||||
explicit expr(const std::string &s, const Iterator &it_begin, const Iterator &it_end) :
|
||||
m_type(TYPE_STRING), it_range(it_begin, it_end) { m_data.s = new std::string(s); }
|
||||
explicit expr(expr &&rhs, const Iterator &it_begin, const Iterator &it_end) : m_type(rhs.type()), it_range{ it_begin, it_end }, m_deferred(rhs.m_deferred)
|
||||
explicit expr(expr &&rhs, const Iterator &it_begin, const Iterator &it_end) : m_type(rhs.type()), it_range{ it_begin, it_end }
|
||||
{
|
||||
m_data.set(rhs.m_data);
|
||||
rhs.m_type = TYPE_EMPTY;
|
||||
rhs.m_deferred = Deferred::None;
|
||||
rhs.m_type = TYPE_EMPTY;
|
||||
}
|
||||
expr &operator=(const expr &rhs)
|
||||
{
|
||||
// set_s() clears m_deferred, which is also rhs.m_deferred on a self-assignment.
|
||||
const Deferred deferred = rhs.m_deferred;
|
||||
if (rhs.type() == TYPE_STRING) {
|
||||
this->set_s(rhs.s());
|
||||
} else {
|
||||
@@ -275,7 +272,6 @@ namespace client
|
||||
m_data.set(rhs.m_data);
|
||||
}
|
||||
this->it_range = rhs.it_range;
|
||||
m_deferred = deferred;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -285,10 +281,8 @@ namespace client
|
||||
this->reset();
|
||||
m_type = rhs.type();
|
||||
this->it_range = rhs.it_range;
|
||||
m_deferred = rhs.m_deferred;
|
||||
m_data.set(rhs.m_data);
|
||||
rhs.m_type = TYPE_EMPTY;
|
||||
rhs.m_deferred = Deferred::None;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
@@ -297,8 +291,7 @@ namespace client
|
||||
{
|
||||
if (this->type() == TYPE_STRING)
|
||||
delete m_data.s;
|
||||
m_type = TYPE_EMPTY;
|
||||
m_deferred = Deferred::None;
|
||||
m_type = TYPE_EMPTY;
|
||||
}
|
||||
~expr() { reset(); }
|
||||
|
||||
@@ -311,10 +304,6 @@ namespace client
|
||||
};
|
||||
Type type() const { return m_type; }
|
||||
bool numeric_type() const { return m_type == TYPE_INT || m_type == TYPE_DOUBLE; }
|
||||
// A deferred string is either one deferred value or text that contains at least one.
|
||||
enum class Deferred : unsigned char { None, Value, Text };
|
||||
bool deferred() const { return m_deferred != Deferred::None; }
|
||||
void set_deferred(Deferred deferred) { m_deferred = deferred; }
|
||||
|
||||
bool& b() { return m_data.b; }
|
||||
bool b() const { return m_data.b; }
|
||||
@@ -334,21 +323,18 @@ namespace client
|
||||
std::string& s() { return *m_data.s; }
|
||||
const std::string& s() const { return *m_data.s; }
|
||||
void set_s(const std::string &s) {
|
||||
m_deferred = Deferred::None;
|
||||
if (this->type() == TYPE_STRING)
|
||||
*m_data.s = s;
|
||||
else
|
||||
this->set_s_take_ownership(new std::string(s));
|
||||
}
|
||||
void set_s(std::string &&s) {
|
||||
m_deferred = Deferred::None;
|
||||
if (this->type() == TYPE_STRING)
|
||||
*m_data.s = std::move(s);
|
||||
else
|
||||
this->set_s_take_ownership(new std::string(std::move(s)));
|
||||
}
|
||||
void set_s(const char *s) {
|
||||
m_deferred = Deferred::None;
|
||||
if (this->type() == TYPE_STRING)
|
||||
*m_data.s = s;
|
||||
else
|
||||
@@ -492,21 +478,12 @@ namespace client
|
||||
{
|
||||
if (this->type() == TYPE_EMPTY) {
|
||||
// Inside an if / else block to be skipped.
|
||||
} else if (m_deferred == Deferred::Value && (rhs.type() != TYPE_STRING || rhs.m_deferred == Deferred::Value)) {
|
||||
// A deferred value added to a number or to another deferred value is meant as a sum.
|
||||
this->throw_if_deferred();
|
||||
} else if (rhs.m_deferred == Deferred::Value && this->type() != TYPE_STRING) {
|
||||
rhs.throw_if_deferred();
|
||||
} else if (this->type() == TYPE_STRING) {
|
||||
// Convert the right hand side to string and append.
|
||||
*m_data.s += rhs.to_string();
|
||||
if (this->deferred() || rhs.deferred())
|
||||
m_deferred = Deferred::Text;
|
||||
} else if (rhs.type() == TYPE_STRING) {
|
||||
// Conver the left hand side to string, append rhs.
|
||||
this->set_s(this->to_string() + rhs.s());
|
||||
if (rhs.deferred())
|
||||
m_deferred = Deferred::Text;
|
||||
} else {
|
||||
const char *err_msg = L("Cannot add non-numeric types.");
|
||||
this->throw_if_not_numeric(err_msg);
|
||||
@@ -625,8 +602,6 @@ namespace client
|
||||
if (lhs.type() == TYPE_EMPTY)
|
||||
// Inside an if / else block to be skipped
|
||||
return;
|
||||
lhs.throw_if_deferred();
|
||||
rhs.throw_if_deferred();
|
||||
bool value = false;
|
||||
if (lhs.numeric_type() && rhs.numeric_type()) {
|
||||
// Both types are numeric.
|
||||
@@ -756,7 +731,6 @@ namespace client
|
||||
if (lhs.type() == TYPE_EMPTY)
|
||||
// Inside an if / else block to be skipped
|
||||
return;
|
||||
lhs.throw_if_deferred();
|
||||
const std::string *subject = nullptr;
|
||||
if (lhs.type() == TYPE_STRING) {
|
||||
// One type is string, the other could be converted to string.
|
||||
@@ -787,7 +761,6 @@ namespace client
|
||||
if (subject.type() == TYPE_EMPTY)
|
||||
// Inside an if / else block to be skipped
|
||||
return;
|
||||
subject.throw_if_deferred();
|
||||
if (subject.type() != TYPE_STRING)
|
||||
subject.throw_exception(L("regex_replace() first parameter must be a string."));
|
||||
try {
|
||||
@@ -812,8 +785,6 @@ namespace client
|
||||
return;
|
||||
}
|
||||
if (! out.b()) {
|
||||
match.throw_if_deferred();
|
||||
pattern.throw_if_deferred();
|
||||
if (match.type() != TYPE_STRING)
|
||||
match.throw_exception(L("one_of(): First parameter (the string to match against) has to be a string value"));
|
||||
if (pattern.type() != TYPE_STRING)
|
||||
@@ -847,8 +818,6 @@ namespace client
|
||||
if (lhs.type() == TYPE_EMPTY)
|
||||
// Inside an if / else block to be skipped
|
||||
return;
|
||||
lhs.throw_if_deferred();
|
||||
rhs.throw_if_deferred();
|
||||
bool value = false;
|
||||
if (lhs.type() == TYPE_BOOL && rhs.type() == TYPE_BOOL) {
|
||||
value = (op == '|') ? (lhs.b() || rhs.b()) : (lhs.b() && rhs.b());
|
||||
@@ -861,16 +830,8 @@ namespace client
|
||||
static void logical_or (expr &lhs, expr &rhs) { logical_op(lhs, rhs, '|'); }
|
||||
static void logical_and(expr &lhs, expr &rhs) { logical_op(lhs, rhs, '&'); }
|
||||
|
||||
static const char* deferred_message()
|
||||
{
|
||||
return L("This value is known only after the G-code is generated. It can be printed, but not used in a calculation or a comparison.");
|
||||
}
|
||||
|
||||
// Reports the deferred message for a type error on a deferred value, since it holds a tag in place of the value.
|
||||
void throw_exception(const char *message) const
|
||||
{
|
||||
if (this->deferred())
|
||||
message = deferred_message();
|
||||
boost::throw_exception(qi::expectation_failure<Iterator>(
|
||||
this->it_range.begin(), this->it_range.end(), spirit::info(std::string("*") + message)));
|
||||
}
|
||||
@@ -881,18 +842,11 @@ namespace client
|
||||
this->throw_exception(message);
|
||||
}
|
||||
|
||||
void throw_if_deferred() const
|
||||
{
|
||||
if (this->deferred())
|
||||
this->throw_exception(deferred_message());
|
||||
}
|
||||
|
||||
private:
|
||||
// This object will take ownership of the parameter string object "s".
|
||||
void set_s_take_ownership(std::string* s) { assert(this->type() != TYPE_STRING); Data tmp; tmp.s = s; m_data.set(tmp); m_type = TYPE_STRING; }
|
||||
|
||||
Type m_type = TYPE_EMPTY;
|
||||
Deferred m_deferred = Deferred::None;
|
||||
|
||||
union Data {
|
||||
bool b;
|
||||
@@ -941,8 +895,6 @@ namespace client
|
||||
std::string error_message;
|
||||
// Local variables declared in {if} branches that were not taken, see PlaceholderParser::check_inactive_branches.
|
||||
mutable std::set<std::string> inactive_local_variables;
|
||||
// Tags of PlaceholderParser::set_deferred(), matched by value so that a copy in a variable is deferred too.
|
||||
const std::set<std::string> *deferred_tags = nullptr;
|
||||
|
||||
// Table to translate symbol tag to a human readable error message.
|
||||
static std::map<std::string, std::string> tag_to_error_message;
|
||||
@@ -1018,18 +970,6 @@ namespace client
|
||||
} else
|
||||
this->config_local.set_key_value(opt_key, opt.release());
|
||||
}
|
||||
void mark_deferred(expr &output) const {
|
||||
if (output.type() != expr::TYPE_STRING)
|
||||
return;
|
||||
expr::Deferred deferred = expr::Deferred::None;
|
||||
if (deferred_tags != nullptr && output.s().find('@') != std::string::npos)
|
||||
for (const std::string &tag : *deferred_tags)
|
||||
if (output.s().find(tag) != std::string::npos) {
|
||||
deferred = output.s() == tag ? expr::Deferred::Value : expr::Deferred::Text;
|
||||
break;
|
||||
}
|
||||
output.set_deferred(deferred);
|
||||
}
|
||||
|
||||
static void legacy_variable_expansion(const MyContext *ctx, IteratorRange &opt_key, std::string &output)
|
||||
{
|
||||
@@ -1490,7 +1430,6 @@ namespace client
|
||||
vector_element_to_expr(ctx, opt, output);
|
||||
else
|
||||
scalar_variable_to_expr(ctx, opt, output);
|
||||
ctx->mark_deferred(output);
|
||||
}
|
||||
output.it_range = opt.it_range;
|
||||
}
|
||||
@@ -1577,7 +1516,7 @@ namespace client
|
||||
if (ctx->skipping()) {
|
||||
} else if (lhs.opt) {
|
||||
if (lhs.opt->is_vector())
|
||||
ctx->throw_exception(L("Cannot assign a scalar value to a vector variable."), rhs.it_range);
|
||||
rhs.throw_exception(L("Cannot assign a scalar value to a vector variable."));
|
||||
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
|
||||
scalar_variable_assign_scalar(ctx, lhs_opt, rhs);
|
||||
} else {
|
||||
@@ -1603,7 +1542,7 @@ namespace client
|
||||
if (ctx->skipping()) {
|
||||
} else if (lhs.opt) {
|
||||
if (lhs.opt->is_scalar())
|
||||
ctx->throw_exception(L("Cannot assign a vector value to a scalar variable."), rhs_value.it_range);
|
||||
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
|
||||
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
|
||||
vector_variable_assign_expr_with_count(ctx, lhs_opt, rhs_count, rhs_value);
|
||||
} else {
|
||||
@@ -1629,7 +1568,7 @@ namespace client
|
||||
if (! ctx->skipping()) {
|
||||
check_writable(ctx, lhs);
|
||||
if (lhs.opt->is_scalar())
|
||||
ctx->throw_exception(L("Cannot assign a vector value to a scalar variable."), rhs_value.it_range);
|
||||
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
|
||||
vector_variable_assign_expr_with_count(ctx, lhs, rhs_count, rhs_value);
|
||||
}
|
||||
}
|
||||
@@ -1864,21 +1803,10 @@ namespace client
|
||||
|
||||
static void filament_change(const MyContext* ctx, expr& param)
|
||||
{
|
||||
if (param.type() == expr::TYPE_EMPTY)
|
||||
// Inside an if / else block to be skipped
|
||||
return;
|
||||
MyContext *context = const_cast<MyContext *>(ctx);
|
||||
param.throw_if_deferred();
|
||||
context->current_extruder_id = param.as_i();
|
||||
}
|
||||
|
||||
// The replacement may put a deferred tag into the result.
|
||||
static void regex_replace(const MyContext *ctx, expr &subject, IteratorRange &pattern, expr &replacement)
|
||||
{
|
||||
expr::regex_replace(subject, pattern, replacement);
|
||||
ctx->mark_deferred(subject);
|
||||
}
|
||||
|
||||
static void throw_exception(const std::string &msg, const IteratorRange &it_range)
|
||||
{
|
||||
// An asterix is added to the start of the string to differentiate the boost::spirit::info::tag content
|
||||
@@ -2481,7 +2409,7 @@ namespace client
|
||||
| (kw["zdigits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1))
|
||||
[ px::bind(&expr::digits<true>, _val, _2, _3) ]
|
||||
| (kw["regex_replace"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > regular_expression > ',' > conditional_expression(_r1) > ')')
|
||||
[ px::bind(&MyContext::regex_replace, _r1, _val, _2, _3) ]
|
||||
[ px::bind(&expr::regex_replace, _val, _2, _3) ]
|
||||
| (kw["int"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::to_int, _1, _val) ]
|
||||
| (kw["round"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::round, _1, _val) ]
|
||||
| (kw["ceil"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::ceil, _1, _val) ]
|
||||
@@ -2686,7 +2614,6 @@ std::string PlaceholderParser::process(const std::string &templ, unsigned int cu
|
||||
context.config = &this->config();
|
||||
context.config_override = config_override;
|
||||
context.config_outputs = config_outputs;
|
||||
context.deferred_tags = &m_deferred_tags;
|
||||
context.current_extruder_id = current_extruder_id;
|
||||
context.context_data = context_data;
|
||||
return process_macro(templ, context);
|
||||
|
||||
@@ -60,9 +60,6 @@ public:
|
||||
void set(const std::string &key, double value) { this->set(key, new ConfigOptionFloat(value)); }
|
||||
void set(const std::string &key, const std::vector<std::string> &values) { this->set(key, new ConfigOptionStrings(values)); }
|
||||
void set(const std::string &key, ConfigOption *opt) { m_config.set_key_value(key, opt); }
|
||||
// Orca: sets a value known only after the G-code is generated, as a tag that post-processing replaces.
|
||||
// A template may print it or join it to text, and a calculation or a comparison on it throws.
|
||||
void set_deferred(const std::string &key, const std::string &tag) { this->set(key, tag); m_deferred_tags.insert(tag); }
|
||||
DynamicConfig& config_writable() { return m_config; }
|
||||
const DynamicConfig& config() const { return m_config; }
|
||||
const ConfigOption* option(const std::string &key) const { return m_config.option(key); }
|
||||
@@ -91,7 +88,6 @@ private:
|
||||
// config has a higher priority than external_config when looking up a symbol.
|
||||
DynamicConfig m_config;
|
||||
const DynamicConfig *m_external_config;
|
||||
std::set<std::string> m_deferred_tags;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -6459,7 +6459,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->label = L("Seam gap");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("In order to reduce the visibility of the seam in a closed loop extrusion, the loop is interrupted and shortened by a specified amount.\n"
|
||||
"This amount can be specified in millimeters or as a percentage of the nozzle diameter. The default value for this parameter is 10%.");
|
||||
"This amount can be specified in millimeters or as a percentage of the current extruder diameter. The default value for this parameter is 10%.");
|
||||
def->sidetext = L("mm or %");
|
||||
def->min = 0;
|
||||
def->mode = comAdvanced;
|
||||
|
||||
@@ -707,6 +707,10 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/PluginManager.hpp
|
||||
plugin/PluginAuditManager.cpp
|
||||
plugin/PluginAuditManager.hpp
|
||||
plugin/PluginAuditManagerGui.cpp
|
||||
plugin/PluginAuditManagerInternal.hpp
|
||||
plugin/PluginModule.cpp
|
||||
plugin/PluginText.hpp
|
||||
plugin/PluginResolver.cpp
|
||||
plugin/PluginResolver.hpp
|
||||
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp
|
||||
@@ -1092,3 +1096,65 @@ if (ORCA_BUILD_PYTHON_STUBGEN_MODULE)
|
||||
PREFIX ""
|
||||
)
|
||||
endif()
|
||||
|
||||
# Headless plugin permission probe. A distinct executable so normal GUI loading and prompt
|
||||
# behavior stay untouched. It compiles the GUI-free plugin sources directly instead of linking
|
||||
# libslic3r_gui, so neither the GUI library nor wx is pulled into the probe. The permission
|
||||
# front end is selected by source composition: PluginAuditManagerGui.cpp for the GUI app and
|
||||
# probe/PluginAuditManagerHeadless.cpp here.
|
||||
#
|
||||
# Ownership: files under plugin/probe/ belong to the probe only; never link them into the GUI
|
||||
# application or libslic3r_gui. Sources listed outside that directory are shared production code
|
||||
# compiled unchanged by both.
|
||||
#
|
||||
# Residual cost: the probe still links libslic3r (the large core library) for Utils,
|
||||
# LifecycleEvents, the model/print types used by the GUI-free host bindings and libslic3r's own
|
||||
# transitive dependencies. It does not link libslic3r_gui, wx or the GUI-only host/plugin sources.
|
||||
# Developer/security-analysis tool, so it is off by default.
|
||||
if (ORCA_BUILD_PLUGIN_PERMISSION_PROBE)
|
||||
add_executable(orca-plugin-probe
|
||||
plugin/probe/PluginPermissionProbe.cpp
|
||||
plugin/probe/PluginProbeCollector.hpp
|
||||
plugin/probe/NanoSvgImplementation.cpp
|
||||
plugin/PluginAuditManager.cpp
|
||||
plugin/PluginAuditManagerInternal.hpp
|
||||
plugin/PluginModule.cpp
|
||||
plugin/PluginText.hpp
|
||||
plugin/probe/PluginAuditManagerHeadless.cpp
|
||||
plugin/PluginFsUtils.cpp
|
||||
plugin/PluginLoader.cpp
|
||||
plugin/PythonInterpreter.cpp
|
||||
plugin/PythonPluginBridge.cpp
|
||||
plugin/probe/PluginHostProbe.cpp
|
||||
plugin/probe/PluginProbeConfig.cpp
|
||||
plugin/host/PluginHostGeometry.cpp
|
||||
plugin/host/PluginHostMesh.cpp
|
||||
plugin/host/PluginHostModel.cpp
|
||||
plugin/host/PluginHostSlicing.cpp
|
||||
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp
|
||||
plugin/pluginTypes/pages/PagesPluginCapability.cpp
|
||||
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.cpp
|
||||
plugin/pluginTypes/script/ScriptPluginCapability.cpp
|
||||
plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp
|
||||
)
|
||||
|
||||
target_compile_definitions(orca-plugin-probe PRIVATE ORCA_PLUGIN_PROBE_BUILD)
|
||||
|
||||
target_include_directories(orca-plugin-probe PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
Utils
|
||||
)
|
||||
|
||||
target_link_libraries(orca-plugin-probe PRIVATE
|
||||
libslic3r
|
||||
nanosvg
|
||||
pybind11::embed
|
||||
)
|
||||
|
||||
set_target_properties(orca-plugin-probe PROPERTIES
|
||||
OUTPUT_NAME orca-plugin-probe
|
||||
FOLDER "tools"
|
||||
)
|
||||
endif()
|
||||
@@ -5632,10 +5632,14 @@ void GLCanvas3D::do_move(const std::string& snapshot_type, bool force_volume_mov
|
||||
// Fixes sinking/flying instances (snaps object to buildplate)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const double shift_z = mo->get_instance_min_z(i.second);
|
||||
//BBS: don't call translate if the z is zero
|
||||
if (mo->auto_drop && (current_printer_technology() == ptSLA || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
if ((current_printer_technology() == ptSLA || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
const Vec3d shift(0.0, 0.0, -shift_z);
|
||||
m_selection.translate(i.first, i.second, shift);
|
||||
mo->translate_instance(i.second, shift);
|
||||
@@ -5759,10 +5763,14 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
|
||||
for (const std::pair<int, int> &i : done) {
|
||||
ModelObject *mo = m_model->objects[i.first];
|
||||
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// BBS: don't call translate if the z is zero
|
||||
const double shift_z = mo->get_instance_min_z(i.second);
|
||||
// leave sinking instances as sinking
|
||||
if (mo->auto_drop && (min_zs.find({i.first, i.second})->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
if ((min_zs.find({i.first, i.second})->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
const Vec3d shift(0.0, 0.0, -shift_z);
|
||||
m_selection.translate(i.first, i.second, shift);
|
||||
mo->translate_instance(i.second, shift);
|
||||
@@ -5857,10 +5865,14 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//BBS: don't call translate if the z is zero
|
||||
double shift_z = mo->get_instance_min_z(i.second);
|
||||
// leave sinking instances as sinking
|
||||
if (mo->auto_drop && (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
if ((min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
Vec3d shift(0.0, 0.0, -shift_z);
|
||||
m_selection.translate(i.first, i.second, shift);
|
||||
mo->translate_instance(i.second, shift);
|
||||
@@ -5973,10 +5985,14 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type)
|
||||
for (const std::pair<int, int>& i : done) {
|
||||
ModelObject* mo = m_model->objects[i.first];
|
||||
|
||||
if (!mo->auto_drop) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//BBS: don't call translate if the z is zero
|
||||
double shift_z = mo->get_instance_min_z(i.second);
|
||||
// leave sinking instances as sinking
|
||||
if (mo->auto_drop && (min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD) && (shift_z != 0.0f)) {
|
||||
if ((min_zs.empty() || min_zs.find({ i.first, i.second })->second >= SINKING_Z_THRESHOLD || shift_z > SINKING_Z_THRESHOLD)&&(shift_z != 0.0f)) {
|
||||
Vec3d shift(0.0, 0.0, -shift_z);
|
||||
m_selection.translate(i.first, i.second, shift);
|
||||
mo->translate_instance(i.second, shift);
|
||||
|
||||
@@ -3002,8 +3002,7 @@ void GUI_App::init_plugin_gui_wiring()
|
||||
};
|
||||
|
||||
// why: a newly loaded plugin only adds a selectable agent
|
||||
// refresh the dropdown and leave the live agent alone, unless the selected preset was
|
||||
// waiting for this agent: plugins load after the startup preset is applied
|
||||
// refresh the dropdown and leave the live agent alone
|
||||
auto refresh_printer_agent_dropdown_after_load = [](const std::string&)
|
||||
{
|
||||
if (!wxTheApp)
|
||||
@@ -3015,11 +3014,8 @@ void GUI_App::init_plugin_gui_wiring()
|
||||
|
||||
app->CallAfter([app]
|
||||
{
|
||||
if (app->is_closing())
|
||||
return;
|
||||
|
||||
app->switch_printer_agent_if_pending();
|
||||
app->refresh_printer_agent_dropdown();
|
||||
if (!app->is_closing())
|
||||
app->refresh_printer_agent_dropdown();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4307,21 +4303,6 @@ void GUI_App::switch_printer_agent()
|
||||
select_machine(effective_agent_id);
|
||||
}
|
||||
|
||||
void GUI_App::switch_printer_agent_if_pending()
|
||||
{
|
||||
if (!m_agent || !preset_bundle)
|
||||
return;
|
||||
|
||||
const std::string wanted_agent_id = resolve_printer_agent_id(preset_bundle->printers.get_edited_preset().config.opt_string("printer_agent"));
|
||||
const auto live_agent = m_agent->get_printer_agent();
|
||||
if (live_agent && live_agent->get_agent_info().id == wanted_agent_id)
|
||||
return;
|
||||
if (!NetworkAgentFactory::get_printer_agent_info(wanted_agent_id))
|
||||
return;
|
||||
|
||||
switch_printer_agent();
|
||||
}
|
||||
|
||||
void GUI_App::select_machine(const std::string& agent_id)
|
||||
{
|
||||
// Skip for BBL agent for now - uses its own device discovery/selection
|
||||
|
||||
@@ -412,9 +412,6 @@ public:
|
||||
|
||||
// Reconcile the live printer agent with the stored preset selection.
|
||||
void switch_printer_agent();
|
||||
// Install the selected preset's agent if it is registered but not live, e.g. when its plugin
|
||||
// finishes loading after the startup preset was applied. Otherwise leave the live agent alone.
|
||||
void switch_printer_agent_if_pending();
|
||||
|
||||
std::string resolve_printer_agent_id(const std::string& stored_id) const;
|
||||
// ORCA TODO: in the future, bbl presets should specify "bbl" printer agent id
|
||||
|
||||
@@ -1618,9 +1618,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
if (obj_idx != -1) {
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||
// Shift on request also when auto drop is off, which ensure_on_bed() skips.
|
||||
ModelObject* mo = (*m_objects)[obj_idx];
|
||||
mo->translate_instances(-mo->min_z() * Vec3d::UnitZ());
|
||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||
cnv->reload_scene(true, true);
|
||||
update_info_items(obj_idx);
|
||||
notify_instance_updated(obj_idx);
|
||||
@@ -6736,9 +6734,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||
int obj_idx, vol_idx;
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
// Shift on request also when auto drop is off, which ensure_on_bed() skips.
|
||||
ModelObject* mo = (*m_objects)[obj_idx];
|
||||
mo->translate_instances(-mo->min_z() * Vec3d::UnitZ());
|
||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||
cnv->reload_scene(true, true);
|
||||
update_info_items(obj_idx);
|
||||
notify_instance_updated(obj_idx);
|
||||
|
||||
@@ -771,17 +771,6 @@ void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) {
|
||||
if (canceled || eptr)
|
||||
return;
|
||||
|
||||
// Nothing could be placed: leave the plates and the instances exactly as they were.
|
||||
// Rebuilding the plates would only re-home the instances by their bounds, which moves
|
||||
// a too large item to whichever plate it happens to reach over.
|
||||
const bool nothing_arranged = !m_selected.empty() &&
|
||||
std::all_of(m_selected.begin(), m_selected.end(), [](const ArrangePolygon &ap) { return !ap.is_arranged(); });
|
||||
if (nothing_arranged) {
|
||||
m_plater->get_notification_manager()->close_notification_of_type(NotificationType::ArrangeOngoing);
|
||||
m_plater->m_arrange_running.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unprintable items go to the last virtual bed
|
||||
int beds = 0;
|
||||
|
||||
@@ -881,13 +870,12 @@ void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) {
|
||||
}
|
||||
m_plater->get_notification_manager()->close_notification_of_type(NotificationType::ArrangeOngoing);
|
||||
|
||||
//BBS: reload all objects due to arrange. Plates are never recycled here: arranging
|
||||
// must not remove a plate the user created, even when the arrange left it empty.
|
||||
//BBS: reload all objects due to arrange
|
||||
if (only_on_partplate) {
|
||||
plate_list.rebuild_plates_after_arrangement(false, true, current_plate_index);
|
||||
plate_list.rebuild_plates_after_arrangement(!only_on_partplate, true, current_plate_index);
|
||||
}
|
||||
else {
|
||||
plate_list.rebuild_plates_after_arrangement(false, true);
|
||||
plate_list.rebuild_plates_after_arrangement(!only_on_partplate, true);
|
||||
}
|
||||
|
||||
// unlock the plates we just locked
|
||||
|
||||
@@ -248,6 +248,7 @@ void FillBedJob::process(Ctl &ctl)
|
||||
if (m_object_idx == -1 || m_selected.empty()) return;
|
||||
|
||||
update_arrange_params(params, m_plater->config(), m_selected);
|
||||
m_bedpts = get_shrink_bedpts(m_plater->config(), params);
|
||||
|
||||
auto &partplate_list = m_plater->get_partplate_list();
|
||||
auto &print = wxGetApp().plater()->get_partplate_list().get_current_fff_print();
|
||||
@@ -259,8 +260,6 @@ void FillBedJob::process(Ctl &ctl)
|
||||
|
||||
update_selected_items_inflation(m_selected, m_plater->config(), params);
|
||||
update_unselected_items_inflation(m_unselected, m_plater->config(), params);
|
||||
// After the inflation, which may give back part of the bed margin.
|
||||
m_bedpts = get_shrink_bedpts(m_plater->config(), params);
|
||||
|
||||
bool do_stop = false;
|
||||
params.stopcondition = [&ctl, &do_stop]() {
|
||||
|
||||
@@ -7048,14 +7048,19 @@ void PartPlateList::postprocess_arrange_polygon(arrangement::ArrangePolygon& arr
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": bed_idx %1%, selected %2%, translation(x) %3%, (y) %4%") % arrange_polygon.bed_idx % selected % unscale<double>(arrange_polygon.translation(X)) % unscale<double>(arrange_polygon.translation(Y));
|
||||
|
||||
// UNARRANGED means the item could not be placed, so it has no bed to map onto and
|
||||
// must be left where the user had it. Moving it to a virtual bed here would shift
|
||||
// it off the plate, because no such plate is created for it afterwards.
|
||||
if (arrange_polygon.bed_idx == arrangement::UNARRANGED)
|
||||
return;
|
||||
|
||||
if ((selected) || (arrange_polygon.bed_idx != PartPlateList::MAX_PLATES_COUNT))
|
||||
{
|
||||
if (arrange_polygon.bed_idx == -1)
|
||||
{
|
||||
// outarea for large object
|
||||
arrange_polygon.bed_idx = m_plate_list.size();
|
||||
BoundingBox apbox = get_extents(arrange_polygon.transformed_poly()); // the item may have been rotated
|
||||
auto apbox_size = apbox.size();
|
||||
|
||||
arrange_polygon.translation(X) = 0.5 * apbox_size[0];
|
||||
arrange_polygon.translation(Y) = scaled<double>(static_cast<double>(m_plate_depth)) - 0.5 * apbox_size[1];
|
||||
}
|
||||
|
||||
arrange_polygon.row = arrange_polygon.bed_idx / m_plate_cols;
|
||||
arrange_polygon.col = arrange_polygon.bed_idx % m_plate_cols;
|
||||
arrange_polygon.translation(X) += scaled<double>(plate_stride_x() * arrange_polygon.col);
|
||||
|
||||
@@ -2865,8 +2865,7 @@ Sidebar::Sidebar(Plater *parent)
|
||||
|
||||
// add printer title
|
||||
scrolled_sizer->Add(p->m_panel_printer_title, 0, wxEXPAND | wxALL, 0);
|
||||
// ORCA Bind with LEFT_DOWN, since closing a dialog over the title bar can send it a stray LEFT_UP
|
||||
auto toggle_printer_section = ([this] (wxMouseEvent & e) {
|
||||
p->m_panel_printer_title->Bind(wxEVT_LEFT_UP, [this] (auto & e) {
|
||||
if (!p || !p->combo_printer || !p->m_text_printer_settings || !p->m_panel_printer_content || !m_scrolled_sizer)
|
||||
return;
|
||||
// ORCA Show printer name on title when its folded to inform user without expanding it again
|
||||
@@ -2877,8 +2876,6 @@ Sidebar::Sidebar(Plater *parent)
|
||||
p->m_panel_printer_separator->Show(isShown);
|
||||
m_scrolled_sizer->Layout();
|
||||
});
|
||||
p->m_panel_printer_title->Bind(wxEVT_LEFT_DOWN , toggle_printer_section);
|
||||
p->m_panel_printer_title->Bind(wxEVT_LEFT_DCLICK, toggle_printer_section);
|
||||
// ORCA add bottom border for seperation wile sections folded
|
||||
p->m_panel_printer_separator = new wxPanel(p->scrolled, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(2))); // ORCA staticline class not works without string
|
||||
p->m_panel_printer_separator->SetBackgroundColour("#FFFFFF");
|
||||
@@ -3251,8 +3248,7 @@ Sidebar::Sidebar(Plater *parent)
|
||||
p->m_panel_filament_title = new StaticBox(p->scrolled, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE);
|
||||
p->m_panel_filament_title->SetBackgroundColor(title_bg);
|
||||
p->m_panel_filament_title->SetBackgroundColor2(0xF1F1F1);
|
||||
// ORCA Bind with LEFT_DOWN, since closing a dialog over the title bar can send it a stray LEFT_UP
|
||||
auto toggle_filament_section = ([this](wxMouseEvent &e) {
|
||||
p->m_panel_filament_title->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent &e) {
|
||||
if (!p || !p->m_filament_area_wrapper || !m_scrolled_sizer || !p->m_bpButton_set_filament || !p->m_purge_mode_btn || !p->m_flushing_volume_btn || !p->m_bpButton_add_filament || !ams_btn)
|
||||
return;
|
||||
// ORCA exclude area of del button from titlebar collapse/expand feature to fix undesired collapse when user spams del filament button
|
||||
@@ -3271,8 +3267,6 @@ Sidebar::Sidebar(Plater *parent)
|
||||
|
||||
CallAfter([this]{update_filaments_counter(true);}); // call after all UI processing done
|
||||
});
|
||||
p->m_panel_filament_title->Bind(wxEVT_LEFT_DOWN , toggle_filament_section);
|
||||
p->m_panel_filament_title->Bind(wxEVT_LEFT_DCLICK, toggle_filament_section);
|
||||
|
||||
wxBoxSizer* bSizer39;
|
||||
bSizer39 = new wxBoxSizer( wxHORIZONTAL );
|
||||
@@ -11040,17 +11034,7 @@ void Plater::priv::split_volume()
|
||||
void Plater::priv::scale_selection_to_fit_print_volume()
|
||||
{
|
||||
#if ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
const BuildVolume volume = this->bed.build_volume();
|
||||
// Every plate shares the bed size, but each sits at its own origin: fit onto the
|
||||
// plate the selection is on instead of always onto the first one.
|
||||
PartPlate* plate = this->partplate_list.get_curr_plate();
|
||||
const Vec3d bed_center = plate != nullptr ? plate->get_center_origin() :
|
||||
Vec3d(volume.bed_center().x(), volume.bed_center().y(), 0.);
|
||||
// On a multi-nozzle printer each nozzle reaches only part of the bed, so a model fitted
|
||||
// to the bed can still exceed a nozzle's printable range. Fit into the volume every
|
||||
// nozzle shares, which is also the area Arrange places items in.
|
||||
const BoundingBoxf3 print_volume = plate != nullptr ? plate->get_build_volume(true) : volume.bounding_volume();
|
||||
this->view3D->get_canvas3d()->get_selection().scale_to_fit_print_volume(volume, Vec2d(bed_center.x(), bed_center.y()), print_volume);
|
||||
this->view3D->get_canvas3d()->get_selection().scale_to_fit_print_volume(this->bed.build_volume());
|
||||
#else
|
||||
this->view3D->get_canvas3d()->get_selection().scale_to_fit_print_volume(*config);
|
||||
#endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/LayOnFace.hpp"
|
||||
#include "slic3r/GUI/GLModel.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -580,12 +579,29 @@ void Selection::drop()
|
||||
|
||||
wxGetApp().plater()->take_snapshot(L("Move object"));
|
||||
|
||||
// do_move() writes every volume back to the model and refreshes the view.
|
||||
setup_cache();
|
||||
TransformationType transformation_type;
|
||||
transformation_type.set_relative();
|
||||
translate(Vec3d(0, 0, -min_z), transformation_type);
|
||||
wxGetApp().plater()->get_view3D_canvas3D()->do_move("");
|
||||
this->move_to_center(Vec3d(0, 0, -min_z));
|
||||
|
||||
for (unsigned int i : m_list) {
|
||||
GLVolume& volume = *(*m_volumes)[i];
|
||||
// Skip the wipe tower: its synthetic id (>= 1000) is not an index into m_model->objects.
|
||||
if (volume.object_idx() >= 1000)
|
||||
continue;
|
||||
ModelObject* model_object = m_model->objects[volume.object_idx()];
|
||||
|
||||
if (model_object != nullptr) {
|
||||
if (m_mode == Volume) {
|
||||
ModelVolume* cur_mv = model_object->volumes[volume.volume_idx()];
|
||||
cur_mv->set_transformation(volume.get_volume_transformation());
|
||||
} else if (m_mode == Instance) {
|
||||
model_object->instances[volume.instance_idx()]->set_transformation(volume.get_instance_transformation());
|
||||
}
|
||||
|
||||
model_object->invalidate_bounding_box();
|
||||
this->notify_instance_update(volume.object_idx(), volume.instance_idx());
|
||||
}
|
||||
}
|
||||
|
||||
wxGetApp().plater()->get_view3D_canvas3D()->post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED));
|
||||
}
|
||||
|
||||
void Selection::center_plate(const int plate_idx) {
|
||||
@@ -1456,9 +1472,11 @@ void Selection::flattening_rotate(const Vec3d& normal)
|
||||
|
||||
for (unsigned int i : m_list) {
|
||||
GLVolume& v = *(*m_volumes)[i];
|
||||
// Additional rotation to align the face normal with the down vector in the world coordinate space.
|
||||
// Normal transformed from the object coordinate space to the world coordinate space.
|
||||
const Geometry::Transformation& old_inst_trafo = v.get_instance_transformation();
|
||||
const Transform3d rotation_matrix = lay_on_face_rotation(normal, old_inst_trafo.get_matrix_no_offset());
|
||||
const Vec3d tnormal = old_inst_trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal;
|
||||
// Additional rotation to align tnormal with the down vector in the world coordinate space.
|
||||
const Transform3d rotation_matrix = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()));
|
||||
v.set_instance_transformation(old_inst_trafo.get_offset_matrix() * rotation_matrix * old_inst_trafo.get_matrix_no_offset());
|
||||
}
|
||||
|
||||
@@ -1478,7 +1496,7 @@ void Selection::scale(const Vec3d& scale, TransformationType transformation_type
|
||||
}
|
||||
|
||||
#if ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
void Selection::scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d& bed_center, const BoundingBoxf3& print_volume)
|
||||
void Selection::scale_to_fit_print_volume(const BuildVolume& volume)
|
||||
{
|
||||
auto fit = [this](double s, Vec3d offset) {
|
||||
if (s <= 0.0 || s == 1.0)
|
||||
@@ -1508,7 +1526,8 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d
|
||||
//wxGetApp().obj_manipul()->set_dirty();
|
||||
};
|
||||
|
||||
auto fit_rectangle = [this, fit, &print_volume]() {
|
||||
auto fit_rectangle = [this, fit](const BuildVolume& volume) {
|
||||
const BoundingBoxf3 print_volume = volume.bounding_volume();
|
||||
const Vec3d print_volume_size = print_volume.size();
|
||||
|
||||
// adds 1/100th of a mm on all sides to avoid false out of print volume detections due to floating-point roundings
|
||||
@@ -1522,7 +1541,7 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d
|
||||
fit(std::min(sx, std::min(sy, sz)), print_volume.center() - get_bounding_box().center());
|
||||
};
|
||||
|
||||
auto fit_circle = [this, fit, &bed_center](const BuildVolume& volume) {
|
||||
auto fit_circle = [this, fit](const BuildVolume& volume) {
|
||||
const Geometry::Circled& print_circle = volume.circle();
|
||||
double print_circle_radius = unscale<double>(print_circle.radius);
|
||||
|
||||
@@ -1553,9 +1572,7 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d
|
||||
const double s = std::min(print_circle_radius / circle_radius, volume.printable_height() / max_z);
|
||||
const Vec3d sel_center = get_bounding_box().center();
|
||||
const Vec3d offset = s * (Vec3d(unscale<double>(circle.center.x()), unscale<double>(circle.center.y()), 0.5 * max_z) - sel_center);
|
||||
// The active plate's bed is the same circle, moved by its centre.
|
||||
const Vec3d plate_shift = Vec3d(bed_center.x() - volume.bed_center().x(), bed_center.y() - volume.bed_center().y(), 0.);
|
||||
const Vec3d print_center = Vec3d(unscale<double>(print_circle.center.x()), unscale<double>(print_circle.center.y()), 0.5 * volume.printable_height()) + plate_shift;
|
||||
const Vec3d print_center = { unscale<double>(print_circle.center.x()), unscale<double>(print_circle.center.y()), 0.5 * volume.printable_height() };
|
||||
fit(s, print_center - (sel_center + offset));
|
||||
};
|
||||
|
||||
@@ -1564,7 +1581,7 @@ void Selection::scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d
|
||||
|
||||
switch (volume.type())
|
||||
{
|
||||
case BuildVolume_Type::Rectangle: { fit_rectangle(); break; }
|
||||
case BuildVolume_Type::Rectangle: { fit_rectangle(volume); break; }
|
||||
case BuildVolume_Type::Circle: { fit_circle(volume); break; }
|
||||
default: { break; }
|
||||
}
|
||||
|
||||
@@ -354,10 +354,7 @@ public:
|
||||
void flattening_rotate(const Vec3d& normal);
|
||||
void scale(const Vec3d& scale, TransformationType transformation_type);
|
||||
#if ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
// bed_center is the centre of the plate the selection is on: every plate shares the
|
||||
// bed size, but each sits at its own origin. print_volume is the box the selection must
|
||||
// fit in, which is the area every nozzle reaches rather than the whole bed.
|
||||
void scale_to_fit_print_volume(const BuildVolume& volume, const Vec2d& bed_center, const BoundingBoxf3& print_volume);
|
||||
void scale_to_fit_print_volume(const BuildVolume& volume);
|
||||
#else
|
||||
void scale_to_fit_print_volume(const DynamicPrintConfig& config);
|
||||
#endif // ENABLE_ENHANCED_PRINT_VOLUME_FIT
|
||||
|
||||
@@ -310,7 +310,7 @@ Label::Label(wxWindow *parent, wxFont const &font, wxString const &text, long st
|
||||
SetBackgroundColour(StaticBox::GetParentBackgroundColor(parent));
|
||||
SetForegroundColour("#262E30");
|
||||
if (style & LB_PROPAGATE_MOUSE_EVENT) {
|
||||
for (auto evt : { wxEVT_LEFT_UP, wxEVT_LEFT_DOWN, wxEVT_LEFT_DCLICK}) // ORCA also propagete LEFT_DCLICK event that used on sidebar titlebars
|
||||
for (auto evt : { wxEVT_LEFT_UP, wxEVT_LEFT_DOWN })
|
||||
Bind(evt, [this] (auto & e) { GetParent()->GetEventHandler()->ProcessEventLocally(e); });
|
||||
};
|
||||
if (style & LB_AUTO_WRAP) {
|
||||
|
||||
@@ -73,14 +73,7 @@ bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode
|
||||
// 3. Fetch slot info and build AmsTrayData directly
|
||||
std::vector<AmsTrayData> trays;
|
||||
int box_count = 0;
|
||||
bool slots_ok = false;
|
||||
try {
|
||||
slots_ok = fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error);
|
||||
} catch (const nlohmann::json::exception& e) {
|
||||
// A slot value of an unexpected type fails the sync instead of escaping to the UI
|
||||
error = e.what();
|
||||
}
|
||||
if (!slots_ok) {
|
||||
if (!fetch_slot_info(device_info.base_url, device_info.api_key, dict, series_id, trays, box_count, error)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "QidiPrinterAgent::fetch_filament_info: Failed to fetch slot info: " << error;
|
||||
return false;
|
||||
}
|
||||
@@ -98,7 +91,7 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
int& box_count,
|
||||
std::string& error)
|
||||
{
|
||||
std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables&multi_color_controller=box");
|
||||
std::string url = join_url(base_url, "/printer/objects/query?save_variables=variables");
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
url += "&box_stepper%20slot" + std::to_string(i) + "=runout_button";
|
||||
}
|
||||
@@ -171,12 +164,9 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
AmsTrayData tray;
|
||||
tray.slot_index = i;
|
||||
|
||||
// Read slot variables. Newer firmware stores a hex color list and a material id instead of dictionary
|
||||
// indexes, and only in the slots it has rewritten, so each slot is checked for both forms.
|
||||
const auto color_value = variables.value("color_slot" + std::to_string(i), nlohmann::json(1));
|
||||
const auto filament_value = variables.value("filament_slot" + std::to_string(i), nlohmann::json(1));
|
||||
const int color_index = color_value.is_number() ? color_value.get<int>() : 0;
|
||||
const int filament_type = filament_value.is_number() ? filament_value.get<int>() : 0;
|
||||
// Read slot variables
|
||||
const int color_index = variables.value("color_slot" + std::to_string(i), 1);
|
||||
const int filament_type = variables.value("filament_slot" + std::to_string(i), 1);
|
||||
const int vendor_type = variables.value("vendor_slot" + std::to_string(i), 0);
|
||||
|
||||
// Check filament presence via runout sensor
|
||||
@@ -197,13 +187,6 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
if (filament_it != dict.filaments.end()) {
|
||||
filament_name = filament_it->second;
|
||||
}
|
||||
if (filament_value.is_string()) {
|
||||
const nlohmann::json::json_pointer decoded_type("/multi_color_controller/box/box" + std::to_string(i / 4) +
|
||||
"_filament_info_" + std::to_string(i % 4) + "_filament_type");
|
||||
if (status.contains(decoded_type) && status[decoded_type].is_string()) {
|
||||
filament_name = status[decoded_type].get<std::string>();
|
||||
}
|
||||
}
|
||||
tray.tray_type = normalize_filament_type(filament_name);
|
||||
|
||||
// Try Qidi-specific setting ID first; fall back to visible preset by type
|
||||
@@ -219,9 +202,7 @@ bool QidiPrinterAgent::fetch_slot_info(const std::string& base_url,
|
||||
|
||||
// Look up color from dictionary
|
||||
auto color_it = dict.colors.find(color_index);
|
||||
if (color_value.is_array() && !color_value.empty() && color_value[0].is_string()) {
|
||||
tray.tray_color = color_value[0].get<std::string>();
|
||||
} else if (color_it != dict.colors.end()) {
|
||||
if (color_it != dict.colors.end()) {
|
||||
tray.tray_color = color_it->second;
|
||||
} else {
|
||||
tray.tray_color = "FFFFFFFF";
|
||||
|
||||
@@ -350,13 +350,9 @@ bool SimplyPrint::do_temp_upload(const boost::filesystem::path& file_path,
|
||||
if (should_open_in_external_browser()) {
|
||||
wxLaunchDefaultBrowser(url);
|
||||
} else {
|
||||
// The tab switch queues a reload of the printer's home page, so switch synchronously
|
||||
// first and queue the import page after it.
|
||||
GUI::wxGetApp().CallAfter([url = wxString::FromUTF8(url)] {
|
||||
const auto mainframe = GUI::wxGetApp().mainframe;
|
||||
mainframe->select_tab(TAB_ID_MONITOR);
|
||||
mainframe->load_printer_url(url);
|
||||
});
|
||||
const auto mainframe = GUI::wxGetApp().mainframe;
|
||||
mainframe->request_select_tab(TAB_ID_MONITOR);
|
||||
mainframe->load_printer_url(url);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "PluginAuditManager.hpp"
|
||||
#include "PluginAuditManagerInternal.hpp"
|
||||
|
||||
#include "../Utils/OrcaCloudServiceAgent.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
@@ -15,25 +16,12 @@
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <slic3r/GUI/BindDialog.hpp>
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/plugin/PluginFsUtils.hpp>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <wx/app.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/msgdlg.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/thread.h>
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/plugin/PluginDescriptor.hpp"
|
||||
#include <Python.h>
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -161,8 +149,10 @@ static bool is_fs_category(AuditEventCategory category)
|
||||
}
|
||||
}
|
||||
|
||||
namespace plugin_audit {
|
||||
|
||||
// The category's name in a saved event grant, e.g. "FsRead" in "FsRead:open".
|
||||
static const char* category_name(AuditEventCategory category)
|
||||
const char* category_name(AuditEventCategory category)
|
||||
{
|
||||
switch (category) {
|
||||
case AuditEventCategory::FsRead: return "FsRead";
|
||||
@@ -179,6 +169,29 @@ static const char* category_name(AuditEventCategory category)
|
||||
return "None";
|
||||
}
|
||||
|
||||
int report_denied(PluginAuditManager& mgr,
|
||||
const std::string& event_name,
|
||||
const AuditDecision& decision,
|
||||
const std::string& target)
|
||||
{
|
||||
AuditViolation violation;
|
||||
violation.plugin_key = mgr.current_plugin();
|
||||
violation.event_name = event_name;
|
||||
violation.reason = decision.reason;
|
||||
mgr.report_violation(violation);
|
||||
|
||||
std::string message = "Plugin attempted audited operation \"" + event_name + "\" without permission";
|
||||
if (!decision.reason.empty())
|
||||
message += ": " + decision.reason;
|
||||
if (!target.empty())
|
||||
message += ": " + target;
|
||||
|
||||
PyErr_SetString(PyExc_PermissionError, message.c_str());
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace plugin_audit
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Path safety
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -247,6 +260,7 @@ 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_capability_name = "";
|
||||
thread_local std::string PluginAuditManager::m_current_audit_phase = "";
|
||||
thread_local std::vector<AllowedRoot> PluginAuditManager::m_scoped_allowed_roots;
|
||||
thread_local std::vector<PyObject*> PluginAuditManager::m_root_frames;
|
||||
thread_local int PluginAuditManager::m_releasing_frames = 0;
|
||||
@@ -316,6 +330,76 @@ std::string PluginAuditManager::current_capability() const { return m_current_ca
|
||||
|
||||
void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); }
|
||||
|
||||
void PluginAuditManager::set_current_audit_phase(const std::string& phase) { m_current_audit_phase = phase; }
|
||||
|
||||
const std::string& PluginAuditManager::current_audit_phase() const { return m_current_audit_phase; }
|
||||
|
||||
void PluginAuditManager::clear_current_audit_phase() { m_current_audit_phase.clear(); }
|
||||
|
||||
void PluginAuditManager::set_observation_observer(AuditObserver observer)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_observer_mutex);
|
||||
m_observation_observer = std::move(observer);
|
||||
}
|
||||
|
||||
void PluginAuditManager::clear_observation_observer()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_observer_mutex);
|
||||
m_observation_observer = nullptr;
|
||||
}
|
||||
|
||||
bool PluginAuditManager::observation_mode() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_observer_mutex);
|
||||
return static_cast<bool>(m_observation_observer);
|
||||
}
|
||||
|
||||
// The audit hook holds the GIL here. Copy the observer out before invoking it so a callback that
|
||||
// reads the manager cannot deadlock on m_observer_mutex.
|
||||
bool PluginAuditManager::record_observation(const std::string& event_name,
|
||||
const std::string& category,
|
||||
const std::vector<std::string>& targets)
|
||||
{
|
||||
AuditObserver observer;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_observer_mutex);
|
||||
observer = m_observation_observer;
|
||||
}
|
||||
if (!observer)
|
||||
return false;
|
||||
|
||||
// The observer is called from a CPython C callback; a C++ exception must not unwind through
|
||||
// the audit hook. The probe's collector only allocates, so a throw here is a lost observation,
|
||||
// not a reason to abort the audited operation.
|
||||
try {
|
||||
AuditObservation observation;
|
||||
observation.plugin_key = m_current_plugin_key;
|
||||
observation.capability = m_current_capability_name;
|
||||
observation.phase = m_current_audit_phase;
|
||||
observation.event_name = event_name;
|
||||
observation.category = category;
|
||||
observation.targets = targets;
|
||||
// Probe mode never denies: the hook returns 0 after recording. A later failure of the
|
||||
// surrounding Python operation is reported by the probe's callback/load result, not here.
|
||||
observation.continued = true;
|
||||
observer(observation);
|
||||
} catch (...) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ScopedPluginAuditPhase::ScopedPluginAuditPhase(std::string phase)
|
||||
: m_phase(std::move(phase))
|
||||
, m_previous_phase(PluginAuditManager::instance().current_audit_phase())
|
||||
{
|
||||
PluginAuditManager::instance().set_current_audit_phase(m_phase);
|
||||
}
|
||||
|
||||
ScopedPluginAuditPhase::~ScopedPluginAuditPhase()
|
||||
{
|
||||
PluginAuditManager::instance().set_current_audit_phase(m_previous_phase);
|
||||
}
|
||||
|
||||
void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root, bool allow_write)
|
||||
{
|
||||
if (root.empty())
|
||||
@@ -500,12 +584,6 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
|
||||
candidate = absolute_candidate;
|
||||
}
|
||||
|
||||
// Importing Python modules reads their source and may inspect package directories. These
|
||||
// reads are normal interpreter work, not plugin data access; keep the exemption read-only
|
||||
// and exclude third-party packages from the stdlib roots.
|
||||
if (!is_write && is_stdlib_path(candidate))
|
||||
return {true, ""};
|
||||
|
||||
// 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.
|
||||
@@ -542,73 +620,14 @@ bool PluginAuditManager::request_filesystem_read_permissions(const std::string&
|
||||
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())
|
||||
// Probe mode records the declaration and proceeds. It must not consult the install state,
|
||||
// open the permission dialog, or persist a grant.
|
||||
if (observation_mode()) {
|
||||
record_observation("orca.request_permissions", "FsRead", paths);
|
||||
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(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
|
||||
wxString::FromUTF8(descriptor.name.c_str()), requested_paths),
|
||||
_L("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(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
|
||||
wxString::FromUTF8(descriptor_name.c_str()), requested_paths),
|
||||
_L("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);
|
||||
return plugin_audit::request_filesystem_read_permissions(plugin_key, paths);
|
||||
}
|
||||
|
||||
void PluginAuditManager::report_violation(const AuditViolation& violation)
|
||||
@@ -870,265 +889,18 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
|
||||
return targets;
|
||||
}
|
||||
|
||||
// The saved list that grants an event: its category's target list, or the event-grant list for
|
||||
// an event with no target. Kept apart, so that no grant doubles as a target of the same name (an
|
||||
// os.fork grant is not an os.exec of a command named "os.fork").
|
||||
std::vector<std::string>* permission_list_for(AuditEventCategory category, bool by_event, PluginPermissions& permissions)
|
||||
{
|
||||
if (by_event)
|
||||
return &permissions.events;
|
||||
|
||||
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;
|
||||
case AuditEventCategory::Threading: return &permissions.threading;
|
||||
case AuditEventCategory::ProcessReplace: return &permissions.process;
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// An event with no target is granted by its effective category and name, e.g. "FsRead:open"
|
||||
// for a read of a file descriptor, which does not grant a write of one ("FsReadWrite:open").
|
||||
std::string event_grant_key(AuditEventCategory category, const std::string& event_name)
|
||||
{
|
||||
return std::string(category_name(category)) + ":" + event_name;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
int report_denied(PluginAuditManager& mgr,
|
||||
const std::string& event_name,
|
||||
const AuditDecision& decision,
|
||||
const std::string& target = {})
|
||||
{
|
||||
AuditViolation violation;
|
||||
violation.plugin_key = mgr.current_plugin();
|
||||
violation.event_name = event_name;
|
||||
violation.reason = decision.reason;
|
||||
mgr.report_violation(violation);
|
||||
|
||||
std::string message = "Plugin attempted audited operation \"" + event_name + "\" without permission";
|
||||
if (!decision.reason.empty())
|
||||
message += ": " + decision.reason;
|
||||
if (!target.empty())
|
||||
message += ": " + target;
|
||||
|
||||
PyErr_SetString(PyExc_PermissionError, message.c_str());
|
||||
return -1;
|
||||
}
|
||||
|
||||
wxString audit_message(AuditEventCategory category, const wxString& plugin_name, const wxString& event_name,
|
||||
const wxString& target_list, size_t target_count)
|
||||
{
|
||||
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_PLURAL("Plugin \"%s\" is requesting to read the following file:\n%s",
|
||||
"Plugin \"%s\" is requesting to read the following files:\n%s", target_count),
|
||||
plugin_name, target_list);
|
||||
case AuditEventCategory::FsReadWrite:
|
||||
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to read/write the following file:\n%s",
|
||||
"Plugin \"%s\" is requesting to read/write the following files:\n%s", target_count),
|
||||
plugin_name, target_list);
|
||||
case AuditEventCategory::FsCreate:
|
||||
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to create the following file:\n%s",
|
||||
"Plugin \"%s\" is requesting to create the following files:\n%s", target_count),
|
||||
plugin_name, target_list);
|
||||
case AuditEventCategory::FsDelete:
|
||||
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to delete the following file:\n%s",
|
||||
"Plugin \"%s\" is requesting to delete the following files:\n%s", target_count),
|
||||
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_PLURAL("Plugin \"%s\" is requesting to run the following command:\n%s",
|
||||
"Plugin \"%s\" is requesting to run the following commands:\n%s", target_count),
|
||||
plugin_name, target_list);
|
||||
case AuditEventCategory::Threading:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting permission to create a thread."), plugin_name);
|
||||
case AuditEventCategory::ProcessReplace:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting to replace the running application with:\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);
|
||||
}
|
||||
}
|
||||
|
||||
// Builds and shows the modal permission prompt. Must run on the GUI thread.
|
||||
bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_name, const std::string& event_name,
|
||||
const std::vector<std::string>& unresolved)
|
||||
{
|
||||
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, unresolved.size()),
|
||||
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
|
||||
return dialog.ShowModal() == wxID_YES;
|
||||
}
|
||||
|
||||
// Records a grant in the plugin's sidecar so it is not asked again. Reads the install state
|
||||
// freshly because the async prompt outlives the caller's stack copy of it.
|
||||
void persist_grant(const std::string& plugin_key, AuditEventCategory category, bool by_event, const std::vector<std::string>& keys)
|
||||
{
|
||||
PluginInstallState state;
|
||||
if (!PluginManager::instance().get_install_state(plugin_key, state))
|
||||
return;
|
||||
|
||||
std::vector<std::string>* permission_list = permission_list_for(category, by_event, state.permissions);
|
||||
if (!permission_list)
|
||||
return;
|
||||
|
||||
for (const auto& key : keys)
|
||||
persist_permission(plugin_key, state, *permission_list, key);
|
||||
}
|
||||
|
||||
static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
|
||||
|
||||
static const char* audit_category_name(AuditEventCategory category)
|
||||
{
|
||||
switch (category) {
|
||||
case AuditEventCategory::FsRead: return "FsRead";
|
||||
case AuditEventCategory::FsReadWrite: return "FsReadWrite";
|
||||
case AuditEventCategory::FsCreate: return "FsCreate";
|
||||
case AuditEventCategory::FsDelete: return "FsDelete";
|
||||
case AuditEventCategory::Http: return "Http";
|
||||
case AuditEventCategory::Socket: return "Socket";
|
||||
case AuditEventCategory::ProcessCreate: return "ProcessCreate";
|
||||
case AuditEventCategory::ProcessReplace: return "ProcessReplace";
|
||||
case AuditEventCategory::Threading: return "Threading";
|
||||
default: return "None";
|
||||
}
|
||||
}
|
||||
|
||||
// Asks the user to grant an audited event, returning true when they approve an inline prompt.
|
||||
// `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has
|
||||
// no target (by_event), which the prompt does not show as a target.
|
||||
//
|
||||
// An audited event can fire on a thread the UI thread may itself be blocked waiting on: the
|
||||
// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and
|
||||
// BackgroundSlicingProcess::stop()/stop_internal() park the UI thread until that worker stops.
|
||||
// Blocking the worker on a marshaled modal -- which is safe for the plugin-load worker that
|
||||
// request_filesystem_read_permissions runs on -- would therefore deadlock the application (the
|
||||
// invariant PluginHostUi.cpp documents for slicing-hook UI calls). Off the main thread the prompt
|
||||
// is therefore posted asynchronously and the current event denied (fail closed, like an unanswered
|
||||
// prompt); the grant is persisted once the user accepts, so a later attempt succeeds without
|
||||
// re-prompting.
|
||||
bool request_permission(AuditEventCategory category, const std::string& plugin_key, const std::string& plugin_name,
|
||||
const std::string& event_name, bool by_event, const std::vector<std::string>& keys)
|
||||
{
|
||||
const std::vector<std::string> shown = by_event ? std::vector<std::string>{} : keys;
|
||||
std::string targets_text = by_event ? "<none>" : "";
|
||||
for (const auto& target : shown) {
|
||||
if (!targets_text.empty())
|
||||
targets_text += ", ";
|
||||
targets_text += target;
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "[AUDIT PROMPT] plugin=" << plugin_key << " event=" << event_name
|
||||
<< " category=" << audit_category_name(category) << " targets=[" << targets_text << "]"
|
||||
<< (by_event && !keys.empty() ? " grant=" + keys.front() : std::string{});
|
||||
|
||||
if (s_permission_prompt_for_testing)
|
||||
return s_permission_prompt_for_testing(event_name, shown);
|
||||
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
|
||||
return false;
|
||||
|
||||
if (wxIsMainThread())
|
||||
return prompt_for_targets(category, plugin_name, event_name, shown);
|
||||
|
||||
GUI::wxGetApp().CallAfter([category, plugin_key, plugin_name, event_name, by_event, keys, shown]() {
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
|
||||
return;
|
||||
if (prompt_for_targets(category, plugin_name, event_name, shown))
|
||||
persist_grant(plugin_key, category, by_event, keys);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decides an event that no allowed root or running operation covers: a saved grant allows it,
|
||||
// otherwise the user is asked and a Yes is saved.
|
||||
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)
|
||||
{
|
||||
const bool by_event = targets.empty();
|
||||
const std::vector<std::string> keys = by_event ? std::vector<std::string>{event_grant_key(category, event_name)} : targets;
|
||||
std::vector<std::string>* permission_list = permission_list_for(category, by_event, state.permissions);
|
||||
|
||||
std::vector<std::string> unresolved = keys;
|
||||
if (permission_list) {
|
||||
unresolved.clear();
|
||||
for (const auto& key : keys)
|
||||
if (!has_permission(*permission_list, key))
|
||||
unresolved.push_back(key);
|
||||
if (unresolved.empty())
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Letting a root frame go runs finalizers inside a scope destructor or another event's hook,
|
||||
// where no dialog may open: what they do must already be granted.
|
||||
if (mgr.releasing_frames())
|
||||
return report_denied(mgr, event_name, {false, "audit permission required"});
|
||||
|
||||
if (!request_permission(category, plugin_key, plugin_name, event_name, by_event, unresolved))
|
||||
return report_denied(mgr, event_name, {false, "audit permission required"});
|
||||
|
||||
if (permission_list)
|
||||
for (const auto& key : unresolved)
|
||||
persist_permission(plugin_key, state, *permission_list, key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Judges an audited event raised while a plugin context is active.
|
||||
int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, AuditEventCategory event_type, PyObject* args)
|
||||
{
|
||||
const bool fs_category = is_fs_category(event_type);
|
||||
const std::vector<std::string> targets = audit_targets(event_name, event_type, args);
|
||||
|
||||
// Probe mode: report the classified event and let it through. Deliberately before the deny
|
||||
// registry and allowed-root checks, so the report covers operations that production would
|
||||
// block as well as those it would allow. No grant is read or written.
|
||||
if (mgr.record_observation(event_name, plugin_audit::category_name(event_type), targets))
|
||||
return 0;
|
||||
|
||||
// A denied path (secrets, certificates, config files -- see is_denied_path) is an
|
||||
// unconditional block: checked before the running-operation check below, so no granted
|
||||
// operation can launder access to one, and before the allowed-root shortcut further down,
|
||||
@@ -1137,7 +909,7 @@ int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, A
|
||||
if (fs_category) {
|
||||
for (const auto& target : targets) {
|
||||
if (mgr.is_denied_path(boost::filesystem::path(target)))
|
||||
return report_denied(mgr, event_name, {false, "denied path"}, target);
|
||||
return plugin_audit::report_denied(mgr, event_name, {false, "denied path"}, target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1162,15 +934,7 @@ int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, A
|
||||
return 0;
|
||||
}
|
||||
|
||||
PluginInstallState state;
|
||||
const bool have_install_state = PluginManager::instance().get_install_state(mgr.current_plugin(), state);
|
||||
if (!have_install_state) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr.current_plugin();
|
||||
}
|
||||
|
||||
const std::string plugin_name = state.plugin_name.empty() ? mgr.current_plugin() : state.plugin_name;
|
||||
|
||||
const int result = decide_audited_event(mgr, state, mgr.current_plugin(), plugin_name, event_name, event_type, targets);
|
||||
const int result = plugin_audit::decide_event(mgr, event_name, event_type, targets);
|
||||
|
||||
// A granted operation covers its nested events while it runs. A targetless grant names no
|
||||
// operation, so it covers nothing else.
|
||||
@@ -1232,18 +996,6 @@ bool PluginAuditManager::is_stdlib_file(const std::string& filename)
|
||||
return it->second;
|
||||
}
|
||||
|
||||
bool PluginAuditManager::is_stdlib_path(const boost::filesystem::path& path)
|
||||
{
|
||||
if (path.empty())
|
||||
return false;
|
||||
|
||||
const auto inside_any = [&path](const std::vector<boost::filesystem::path>& dirs) {
|
||||
return std::any_of(dirs.begin(), dirs.end(),
|
||||
[&path](const boost::filesystem::path& dir) { return is_inside_allowed_root(path, dir); });
|
||||
};
|
||||
return inside_any(PluginAuditDetail::s_stdlib_dirs) && !inside_any(PluginAuditDetail::s_site_package_dirs);
|
||||
}
|
||||
|
||||
void PluginAuditManager::release_frames(std::vector<PyObject*>& frames)
|
||||
{
|
||||
if (frames.empty())
|
||||
@@ -1269,11 +1021,6 @@ void PluginAuditManager::release_frames(std::vector<PyObject*>& frames)
|
||||
PyErr_SetRaisedException(error);
|
||||
}
|
||||
|
||||
void PluginAuditManager::set_permission_prompt_for_testing(PermissionPrompt prompt)
|
||||
{
|
||||
PluginAuditDetail::s_permission_prompt_for_testing = std::move(prompt);
|
||||
}
|
||||
|
||||
int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data)
|
||||
{
|
||||
auto* mgr = static_cast<PluginAuditManager*>(user_data);
|
||||
@@ -1312,7 +1059,7 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
|
||||
|
||||
void PluginAuditManager::install_hook()
|
||||
{
|
||||
// Identifies stdlib frames for operation roots and stdlib paths for read-only access.
|
||||
// Tells the stdlib's own frames apart, for the operation-root frames (see record_root_frame).
|
||||
PluginAuditDetail::collect_stdlib_dirs();
|
||||
|
||||
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It
|
||||
|
||||
@@ -24,6 +24,19 @@ struct AuditViolation {
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
// One classified CPython audit event observed while the probe is in record-only mode. This is
|
||||
// evidence about the code path that ran, not a grant request: the hook allowed the operation to
|
||||
// continue and did not consult the permission dialog or the persisted install state.
|
||||
struct AuditObservation {
|
||||
std::string plugin_key;
|
||||
std::string capability;
|
||||
std::string phase;
|
||||
std::string event_name;
|
||||
std::string category;
|
||||
std::vector<std::string> targets;
|
||||
bool continued = true;
|
||||
};
|
||||
|
||||
// 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.
|
||||
@@ -137,6 +150,30 @@ public:
|
||||
bool request_filesystem_read_permissions(const std::string& plugin_key,
|
||||
const std::vector<std::string>& paths);
|
||||
|
||||
// --- probe observation mode (thread-safe, never used by the GUI) ---
|
||||
// When an observer is installed, every classified event raised by plugin-authored code is
|
||||
// reported to it and then allowed through (the hook returns 0). The permission dialogs, saved
|
||||
// grants and .install_state.json are never consulted, so this is not a containment boundary.
|
||||
// The observer must not touch Python objects: it runs inside the audit hook with the GIL held.
|
||||
using AuditObserver = std::function<void(const AuditObservation&)>;
|
||||
void set_observation_observer(AuditObserver observer);
|
||||
void clear_observation_observer();
|
||||
bool observation_mode() const;
|
||||
|
||||
// Stop after classification/target extraction; true means an observer consumed the event.
|
||||
// Exposed for the probe and its tests, not for production callers.
|
||||
bool record_observation(const std::string& event_name,
|
||||
const std::string& category,
|
||||
const std::vector<std::string>& targets);
|
||||
|
||||
// --- probe phase attribution (thread_local) ---
|
||||
// Labels events for the probe's report. Production leaves it empty. A Python thread spawned by
|
||||
// plugin code inherits no phase, so its events are reported as unattributed rather than being
|
||||
// guessed at.
|
||||
void set_current_audit_phase(const std::string& phase);
|
||||
const std::string& current_audit_phase() const;
|
||||
void clear_current_audit_phase();
|
||||
|
||||
void report_violation(const AuditViolation& violation);
|
||||
bool audit_denial_pending() const;
|
||||
void clear_audit_denial();
|
||||
@@ -173,16 +210,12 @@ private:
|
||||
|
||||
static int audit_hook(const char* event, PyObject* args, void* user_data);
|
||||
|
||||
// True for paths inside the interpreter's stdlib, excluding site-packages. Unlike
|
||||
// is_stdlib_file(), this query is not cached because filesystem paths may resolve through
|
||||
// symlinks that can change.
|
||||
static bool is_stdlib_path(const boost::filesystem::path& path);
|
||||
|
||||
// Drops the references in `frames`, keeping the caller's error and denial state intact.
|
||||
void release_frames(std::vector<PyObject*>& frames);
|
||||
|
||||
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_audit_phase;
|
||||
static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots;
|
||||
static thread_local std::vector<PyObject*> m_root_frames; // strong references
|
||||
static thread_local int m_releasing_frames;
|
||||
@@ -195,6 +228,11 @@ private:
|
||||
std::vector<AllowedRoot> m_global_allowed_roots;
|
||||
std::vector<std::string> m_denied_filenames;
|
||||
std::vector<std::string> m_denied_path_keywords;
|
||||
|
||||
// Kept apart from m_mutex: observation_mode() is queried from the audit hook while the hook
|
||||
// already holds the GIL, and the setter runs before plugin code starts.
|
||||
mutable std::mutex m_observer_mutex;
|
||||
AuditObserver m_observation_observer;
|
||||
};
|
||||
|
||||
// RAII guard that sets the current plugin key and capability name, restoring the previous
|
||||
@@ -225,6 +263,22 @@ private:
|
||||
std::vector<PyObject*> m_previous_root_frames;
|
||||
};
|
||||
|
||||
// Sets the probe's phase label on the current thread and restores the previous one on scope exit.
|
||||
// Empty in production, where no phase is ever set.
|
||||
class ScopedPluginAuditPhase
|
||||
{
|
||||
public:
|
||||
explicit ScopedPluginAuditPhase(std::string phase);
|
||||
~ScopedPluginAuditPhase();
|
||||
|
||||
ScopedPluginAuditPhase(const ScopedPluginAuditPhase&) = delete;
|
||||
ScopedPluginAuditPhase& operator=(const ScopedPluginAuditPhase&) = delete;
|
||||
|
||||
private:
|
||||
std::string m_phase;
|
||||
std::string m_previous_phase;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_PluginAuditManager_hpp_
|
||||
@@ -0,0 +1,332 @@
|
||||
#include "PluginAuditManagerInternal.hpp"
|
||||
|
||||
#include "PluginDescriptor.hpp"
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PluginManager.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <slic3r/plugin/PluginAuditManager.hpp>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/I18N.hpp>
|
||||
|
||||
#include <wx/app.h>
|
||||
#include <wx/msgdlg.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/thread.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
// The saved list that grants an event: its category's target list, or the event-grant list for
|
||||
// an event with no target. Kept apart, so that no grant doubles as a target of the same name (an
|
||||
// os.fork grant is not an os.exec of a command named "os.fork").
|
||||
std::vector<std::string>* permission_list_for(AuditEventCategory category, bool by_event, PluginPermissions& permissions)
|
||||
{
|
||||
if (by_event)
|
||||
return &permissions.events;
|
||||
|
||||
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;
|
||||
case AuditEventCategory::Threading: return &permissions.threading;
|
||||
case AuditEventCategory::ProcessReplace: return &permissions.process;
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// An event with no target is granted by its effective category and name, e.g. "FsRead:open"
|
||||
// for a read of a file descriptor, which does not grant a write of one ("FsReadWrite:open").
|
||||
std::string event_grant_key(AuditEventCategory category, const std::string& event_name)
|
||||
{
|
||||
return std::string(plugin_audit::category_name(category)) + ":" + event_name;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
wxString audit_message(AuditEventCategory category, const wxString& plugin_name, const wxString& event_name,
|
||||
const wxString& target_list, size_t target_count)
|
||||
{
|
||||
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_PLURAL("Plugin \"%s\" is requesting to read the following file:\n%s",
|
||||
"Plugin \"%s\" is requesting to read the following files:\n%s", target_count),
|
||||
plugin_name, target_list);
|
||||
case AuditEventCategory::FsReadWrite:
|
||||
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to read/write the following file:\n%s",
|
||||
"Plugin \"%s\" is requesting to read/write the following files:\n%s", target_count),
|
||||
plugin_name, target_list);
|
||||
case AuditEventCategory::FsCreate:
|
||||
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to create the following file:\n%s",
|
||||
"Plugin \"%s\" is requesting to create the following files:\n%s", target_count),
|
||||
plugin_name, target_list);
|
||||
case AuditEventCategory::FsDelete:
|
||||
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to delete the following file:\n%s",
|
||||
"Plugin \"%s\" is requesting to delete the following files:\n%s", target_count),
|
||||
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_PLURAL("Plugin \"%s\" is requesting to run the following command:\n%s",
|
||||
"Plugin \"%s\" is requesting to run the following commands:\n%s", target_count),
|
||||
plugin_name, target_list);
|
||||
case AuditEventCategory::Threading:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting permission to create a thread."), plugin_name);
|
||||
case AuditEventCategory::ProcessReplace:
|
||||
return wxString::Format(_L("Plugin \"%s\" is requesting to replace the running application with:\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);
|
||||
}
|
||||
}
|
||||
|
||||
// Builds and shows the modal permission prompt. Must run on the GUI thread.
|
||||
bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_name, const std::string& event_name,
|
||||
const std::vector<std::string>& unresolved)
|
||||
{
|
||||
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, unresolved.size()),
|
||||
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
|
||||
return dialog.ShowModal() == wxID_YES;
|
||||
}
|
||||
|
||||
// Records a grant in the plugin's sidecar so it is not asked again. Reads the install state
|
||||
// freshly because the async prompt outlives the caller's stack copy of it.
|
||||
void persist_grant(const std::string& plugin_key, AuditEventCategory category, bool by_event, const std::vector<std::string>& keys)
|
||||
{
|
||||
PluginInstallState state;
|
||||
if (!PluginManager::instance().get_install_state(plugin_key, state))
|
||||
return;
|
||||
|
||||
std::vector<std::string>* permission_list = permission_list_for(category, by_event, state.permissions);
|
||||
if (!permission_list)
|
||||
return;
|
||||
|
||||
for (const auto& key : keys)
|
||||
persist_permission(plugin_key, state, *permission_list, key);
|
||||
}
|
||||
|
||||
PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
|
||||
|
||||
// Asks the user to grant an audited event, returning true when they approve an inline prompt.
|
||||
// `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has
|
||||
// no target (by_event), which the prompt does not show as a target.
|
||||
//
|
||||
// An audited event can fire on a thread the UI thread may itself be blocked waiting on: the
|
||||
// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and
|
||||
// BackgroundSlicingProcess::stop()/stop_internal() park the UI thread until that worker stops.
|
||||
// Blocking the worker on a marshaled modal -- which is safe for the plugin-load worker that
|
||||
// request_filesystem_read_permissions runs on -- would therefore deadlock the application (the
|
||||
// invariant PluginHostUi.cpp documents for slicing-hook UI calls). Off the main thread the prompt
|
||||
// is therefore posted asynchronously and the current event denied (fail closed, like an unanswered
|
||||
// prompt); the grant is persisted once the user accepts, so a later attempt succeeds without
|
||||
// re-prompting.
|
||||
bool request_permission(AuditEventCategory category, const std::string& plugin_key, const std::string& plugin_name,
|
||||
const std::string& event_name, bool by_event, const std::vector<std::string>& keys)
|
||||
{
|
||||
const std::vector<std::string> shown = by_event ? std::vector<std::string>{} : keys;
|
||||
|
||||
if (s_permission_prompt_for_testing)
|
||||
return s_permission_prompt_for_testing(event_name, shown);
|
||||
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
|
||||
return false;
|
||||
|
||||
if (wxIsMainThread())
|
||||
return prompt_for_targets(category, plugin_name, event_name, shown);
|
||||
|
||||
GUI::wxGetApp().CallAfter([category, plugin_key, plugin_name, event_name, by_event, keys, shown]() {
|
||||
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
|
||||
return;
|
||||
if (prompt_for_targets(category, plugin_name, event_name, shown))
|
||||
persist_grant(plugin_key, category, by_event, keys);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decides an event that no allowed root or running operation covers: a saved grant allows it,
|
||||
// otherwise the user is asked and a Yes is saved.
|
||||
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)
|
||||
{
|
||||
const bool by_event = targets.empty();
|
||||
const std::vector<std::string> keys = by_event ? std::vector<std::string>{event_grant_key(category, event_name)} : targets;
|
||||
std::vector<std::string>* permission_list = permission_list_for(category, by_event, state.permissions);
|
||||
|
||||
std::vector<std::string> unresolved = keys;
|
||||
if (permission_list) {
|
||||
unresolved.clear();
|
||||
for (const auto& key : keys)
|
||||
if (!has_permission(*permission_list, key))
|
||||
unresolved.push_back(key);
|
||||
if (unresolved.empty())
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Letting a root frame go runs finalizers inside a scope destructor or another event's hook,
|
||||
// where no dialog may open: what they do must already be granted.
|
||||
if (mgr.releasing_frames())
|
||||
return plugin_audit::report_denied(mgr, event_name, {false, "audit permission required"});
|
||||
|
||||
if (!request_permission(category, plugin_key, plugin_name, event_name, by_event, unresolved))
|
||||
return plugin_audit::report_denied(mgr, event_name, {false, "audit permission required"});
|
||||
|
||||
if (permission_list)
|
||||
for (const auto& key : unresolved)
|
||||
persist_permission(plugin_key, state, *permission_list, key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool plugin_audit::request_filesystem_read_permissions(const std::string& plugin_key,
|
||||
const std::vector<std::string>& paths)
|
||||
{
|
||||
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(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
|
||||
wxString::FromUTF8(descriptor.name.c_str()), requested_paths),
|
||||
_L("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(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
|
||||
wxString::FromUTF8(descriptor_name.c_str()), requested_paths),
|
||||
_L("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);
|
||||
}
|
||||
|
||||
int plugin_audit::decide_event(PluginAuditManager& mgr,
|
||||
const std::string& event_name,
|
||||
AuditEventCategory category,
|
||||
const std::vector<std::string>& targets)
|
||||
{
|
||||
PluginInstallState state;
|
||||
const bool have_install_state = PluginManager::instance().get_install_state(mgr.current_plugin(), state);
|
||||
if (!have_install_state) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr.current_plugin();
|
||||
}
|
||||
|
||||
const std::string plugin_name = state.plugin_name.empty() ? mgr.current_plugin() : state.plugin_name;
|
||||
|
||||
return decide_audited_event(mgr, state, mgr.current_plugin(), plugin_name, event_name, category, targets);
|
||||
}
|
||||
|
||||
void PluginAuditManager::set_permission_prompt_for_testing(PermissionPrompt prompt)
|
||||
{
|
||||
s_permission_prompt_for_testing = std::move(prompt);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifndef slic3r_PluginAuditManagerInternal_hpp_
|
||||
#define slic3r_PluginAuditManagerInternal_hpp_
|
||||
|
||||
// Internal seam between the GUI-free audit core and the permission front end. The GUI library
|
||||
// supplies the dialogs, prompts and .install_state.json persistence; the headless probe supplies
|
||||
// a fail-closed implementation that never links wx. Production behavior lives in the GUI
|
||||
// implementation, which is the code that used to be part of PluginAuditManager.cpp.
|
||||
|
||||
#include "PluginAuditManager.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r::plugin_audit {
|
||||
|
||||
const char* category_name(AuditEventCategory category);
|
||||
|
||||
int report_denied(PluginAuditManager& mgr,
|
||||
const std::string& event_name,
|
||||
const AuditDecision& decision,
|
||||
const std::string& target = {});
|
||||
|
||||
// Used only when the manager is not in observation mode. The GUI implementation consults the
|
||||
// install state, prompts and persists; the probe implementation fails closed.
|
||||
bool request_filesystem_read_permissions(const std::string& plugin_key,
|
||||
const std::vector<std::string>& paths);
|
||||
|
||||
// Decides an event that no allowed root or running operation covered. The GUI implementation
|
||||
// reads the install state and prompts; the probe implementation fails closed. In observation
|
||||
// mode the audit hook records and returns before reaching this seam.
|
||||
int decide_event(PluginAuditManager& mgr,
|
||||
const std::string& event_name,
|
||||
AuditEventCategory category,
|
||||
const std::vector<std::string>& targets);
|
||||
|
||||
} // namespace Slic3r::plugin_audit
|
||||
|
||||
#endif // slic3r_PluginAuditManagerInternal_hpp_
|
||||
@@ -14,8 +14,7 @@
|
||||
|
||||
#include "PluginAuditManager.hpp"
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
#include "PluginText.hpp"
|
||||
|
||||
#include <exception>
|
||||
#include <miniz.h>
|
||||
@@ -92,7 +91,7 @@ bool resolve_allowed_plugin_root(const PluginDescriptor& descriptor,
|
||||
|
||||
const fs::path plugin_root = resolve_plugin_root_from_descriptor(descriptor);
|
||||
if (plugin_root.empty()) {
|
||||
error = _u8L("Plugin folder could not be determined.");
|
||||
error = PLUGIN_U8L("Plugin folder could not be determined.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -103,7 +102,7 @@ bool resolve_allowed_plugin_root(const PluginDescriptor& descriptor,
|
||||
resolved_root = fs::absolute(plugin_root, ec);
|
||||
}
|
||||
if (ec || resolved_root.empty()) {
|
||||
error = GUI::format(_u8L("Failed to resolve plugin folder: %1%"), plugin_root.string());
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to resolve plugin folder: %1%"), plugin_root.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -123,7 +122,7 @@ bool delete_plugin_root(const boost::filesystem::path& resolved_root, const std:
|
||||
const auto removed_count = fs::remove_all(resolved_root, ec);
|
||||
if (ec) {
|
||||
// TRN %1% is a folder path, %2% the system error message.
|
||||
error = GUI::format(_u8L("Failed to delete plugin folder %1%: %2%"), resolved_root.string(), ec.message());
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to delete plugin folder %1%: %2%"), resolved_root.string(), ec.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1199,7 +1198,7 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
if (!fs::exists(plugin_dir) || !fs::is_directory(plugin_dir)) {
|
||||
error = GUI::format(_u8L("Plugin directory does not exist: %1%"), plugin_dir.string());
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin directory does not exist: %1%"), plugin_dir.string());
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1216,7 +1215,7 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
|
||||
if (ext == ".py") {
|
||||
if (!py_entry.empty()) {
|
||||
// TRN %1% and %2% are file names.
|
||||
error = GUI::format(_u8L("Plugin directory contains multiple .py files: %1% and %2%"),
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin directory contains multiple .py files: %1% and %2%"),
|
||||
py_entry.filename().string(), it->path().filename().string());
|
||||
return {};
|
||||
}
|
||||
@@ -1224,7 +1223,7 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
|
||||
} else if (ext == ".whl") {
|
||||
if (!whl_entry.empty()) {
|
||||
// TRN %1% and %2% are file names.
|
||||
error = GUI::format(_u8L("Plugin directory contains multiple .whl files: %1% and %2%"),
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin directory contains multiple .whl files: %1% and %2%"),
|
||||
whl_entry.filename().string(), it->path().filename().string());
|
||||
return {};
|
||||
}
|
||||
@@ -1233,7 +1232,7 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
|
||||
}
|
||||
|
||||
if (!py_entry.empty() && !whl_entry.empty()) {
|
||||
error = _u8L("Plugin directory contains both .py and .whl entry files");
|
||||
error = PLUGIN_U8L("Plugin directory contains both .py and .whl entry files");
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1243,7 +1242,7 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
|
||||
if (!whl_entry.empty())
|
||||
return whl_entry;
|
||||
|
||||
error = _u8L("Plugin directory does not contain a .py or .whl entry file");
|
||||
error = PLUGIN_U8L("Plugin directory does not contain a .py or .whl entry file");
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1260,34 +1259,34 @@ std::vector<std::string> collect_resource_texture_files(const std::string& dir,
|
||||
error.clear();
|
||||
|
||||
if (dir.empty()) {
|
||||
error = _u8L("Resource texture directory is not set");
|
||||
error = PLUGIN_U8L("Resource texture directory is not set");
|
||||
return files;
|
||||
}
|
||||
if (plugin_root.empty()) {
|
||||
error = _u8L("Plugin folder could not be determined.");
|
||||
error = PLUGIN_U8L("Plugin folder could not be determined.");
|
||||
return files;
|
||||
}
|
||||
|
||||
boost::system::error_code ec;
|
||||
const fs::path resolved_dir = fs::weakly_canonical(fs::path(dir), ec);
|
||||
if (ec) {
|
||||
error = _u8L("Resource texture directory could not be resolved");
|
||||
error = PLUGIN_U8L("Resource texture directory could not be resolved");
|
||||
return files;
|
||||
}
|
||||
|
||||
if (!fs::is_directory(resolved_dir, ec) || ec) {
|
||||
error = _u8L("Resource texture path is not a directory");
|
||||
error = PLUGIN_U8L("Resource texture path is not a directory");
|
||||
return files;
|
||||
}
|
||||
|
||||
const fs::path resolved_root = fs::weakly_canonical(fs::path(plugin_root), ec);
|
||||
if (ec) {
|
||||
error = _u8L("Plugin folder could not be resolved");
|
||||
error = PLUGIN_U8L("Plugin folder could not be resolved");
|
||||
return files;
|
||||
}
|
||||
|
||||
if (!is_inside_allowed_root(resolved_dir, resolved_root)) {
|
||||
error = _u8L("Resource texture directory is outside the plugin folder");
|
||||
error = PLUGIN_U8L("Resource texture directory is outside the plugin folder");
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -1312,7 +1311,7 @@ std::vector<std::string> collect_resource_texture_files(const std::string& dir,
|
||||
}
|
||||
|
||||
if (ec) {
|
||||
error = _u8L("Resource texture directory could not be read");
|
||||
error = PLUGIN_U8L("Resource texture directory could not be read");
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
#include "PythonPluginBridge.hpp"
|
||||
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
#include "PluginText.hpp"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
@@ -102,7 +101,7 @@ bool read_local_plugin_package_metadata(const boost::filesystem::path& source_pa
|
||||
|
||||
const std::string ext = plugin_package_extension(source_path);
|
||||
if (ext != ".py" && ext != ".whl") {
|
||||
error = GUI::format(_u8L("Plugin package must be a .py or .whl file, got: %1%"), ext);
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin package must be a .py or .whl file, got: %1%"), ext);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -179,14 +178,14 @@ bool add_wheel_dependencies_to_sys_path(const PluginDescriptor& descriptor,
|
||||
std::string extract_error;
|
||||
if (!extract_zip_to_directory(it->path(), dep_dir, extract_error)) {
|
||||
// TRN %1% is the dependency file, %2% the error detail.
|
||||
error = GUI::format(_u8L("Failed to extract plugin .whl dependency %1%: %2%"), it->path().string(), extract_error);
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to extract plugin .whl dependency %1%: %2%"), it->path().string(), extract_error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::string syspath_error;
|
||||
if (!interpreter.add_plugin_sys_path(dep_dir.string(), syspath_error)) {
|
||||
error = GUI::format(_u8L("Failed to add .whl dependency to sys.path: %1%"), syspath_error);
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to add .whl dependency to sys.path: %1%"), syspath_error);
|
||||
return false;
|
||||
}
|
||||
plugin_paths.push_back(dep_dir.string());
|
||||
@@ -248,12 +247,12 @@ bool load(const PluginDescriptor& descriptor,
|
||||
|
||||
PythonInterpreter& interpreter = PythonInterpreter::instance();
|
||||
if (!interpreter.is_initialized()) {
|
||||
error = GUI::format(_u8L("Python interpreter not initialized: %1%"), interpreter.last_error());
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Python interpreter not initialized: %1%"), interpreter.last_error());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!descriptor.is_metadata_valid()) {
|
||||
error = GUI::format(_u8L("Plugin manifest is invalid: %1%"), descriptor.plugin_key);
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin manifest is invalid: %1%"), descriptor.plugin_key);
|
||||
if (descriptor.has_error())
|
||||
error += " - " + descriptor.normalized_error();
|
||||
return false;
|
||||
@@ -274,7 +273,7 @@ bool load(const PluginDescriptor& descriptor,
|
||||
if (!skip_deps) {
|
||||
std::string pkg_install_error;
|
||||
if (!install_packages(descriptor.dependencies, pkg_install_error)) {
|
||||
error = GUI::format(_u8L("Failed to install plugin dependencies: %1%"), pkg_install_error);
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to install plugin dependencies: %1%"), pkg_install_error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -293,7 +292,7 @@ bool load(const PluginDescriptor& descriptor,
|
||||
PyObject* module = import_plugin_module(descriptor, plugin.plugin_sys_paths, plugin.plugin_modules, load_error);
|
||||
if (module == nullptr) {
|
||||
bridge.cancel_plugin_capture(descriptor.entry_path);
|
||||
error = GUI::format(_u8L("Failed to load plugin module: %1%"), load_error);
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to load plugin module: %1%"), load_error);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -308,13 +307,13 @@ bool load(const PluginDescriptor& descriptor,
|
||||
auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, descriptor.plugin_key, bridge_error);
|
||||
if (!bridge_error.empty()) {
|
||||
capabilities_found.clear();
|
||||
error = GUI::format(_u8L("Plugin registration failed: %1%"), bridge_error);
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin registration failed: %1%"), bridge_error);
|
||||
if (registration_failed != nullptr)
|
||||
*registration_failed = true;
|
||||
return false;
|
||||
}
|
||||
if (capabilities_found.empty()) {
|
||||
error = _u8L("Plugin module did not register any capabilities");
|
||||
error = PLUGIN_U8L("Plugin module did not register any capabilities");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -366,7 +365,7 @@ bool load(const PluginDescriptor& descriptor,
|
||||
|
||||
if (!seen_capabilities[type].insert(found.name).second) {
|
||||
// TRN %1% is the capability name, %2% the capability type (e.g. Script).
|
||||
materialization_error = GUI::format(_u8L("Plugin declares duplicate capability '%1%' for type %2%"),
|
||||
materialization_error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin declares duplicate capability '%1%' for type %2%"),
|
||||
found.name, PluginConfig::capability_type_label(type));
|
||||
break;
|
||||
}
|
||||
@@ -422,9 +421,9 @@ bool load(const PluginDescriptor& descriptor,
|
||||
capabilities.push_back(std::move(instance));
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
materialization_error = GUI::format(_u8L("Plugin capability materialization failed: %1%"), ex.what());
|
||||
materialization_error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin capability materialization failed: %1%"), ex.what());
|
||||
} catch (...) {
|
||||
materialization_error = _u8L("Plugin capability materialization failed");
|
||||
materialization_error = PLUGIN_U8L("Plugin capability materialization failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -466,11 +465,11 @@ bool load(const PluginDescriptor& descriptor,
|
||||
} catch (const std::exception& ex) {
|
||||
teardown_capabilities(plugin.capabilities, lifecycle_count);
|
||||
// TRN on_load is the name of the plugin's startup function; keep it untranslated.
|
||||
error = GUI::format(_u8L("Plugin on_load failed: %1%"), ex.what());
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin on_load failed: %1%"), ex.what());
|
||||
return false;
|
||||
} catch (...) {
|
||||
teardown_capabilities(plugin.capabilities, lifecycle_count);
|
||||
error = _u8L("Plugin on_load failed");
|
||||
error = PLUGIN_U8L("Plugin on_load failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -651,7 +650,7 @@ bool install_plugin(const boost::filesystem::path& filepath,
|
||||
bool backup_created = false;
|
||||
|
||||
auto fail = [&error](const std::string& message) {
|
||||
error = GUI::format(_u8L("Plugin installation failed: %1%"), message);
|
||||
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin installation failed: %1%"), message);
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -659,23 +658,23 @@ bool install_plugin(const boost::filesystem::path& filepath,
|
||||
const fs::path source_path = canonical_or_absolute(filepath);
|
||||
|
||||
if (!fs::exists(source_path, ec) || !fs::is_regular_file(source_path, ec))
|
||||
return fail(GUI::format(_u8L("Plugin package is not a file: %1%"), filepath.string()));
|
||||
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Plugin package is not a file: %1%"), filepath.string()));
|
||||
|
||||
const std::string ext = plugin_package_extension(source_path);
|
||||
if (ext != ".py" && ext != ".whl")
|
||||
return fail(GUI::format(_u8L("Plugin package must be a .py or .whl file, got: %1%"), ext));
|
||||
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Plugin package must be a .py or .whl file, got: %1%"), ext));
|
||||
// The cloud UUID is concatenated into the install path (final_dir = plugin_root / cloud_uuid).
|
||||
// It comes verbatim from the server's catalog JSON, so reject anything that could escape the
|
||||
// per-user plugin root (traversal, leading dots, separators) before touching the filesystem.
|
||||
if (is_cloud_install && !is_valid_plugin_id(cloud_uuid))
|
||||
return fail(GUI::format(_u8L("Cloud plugin UUID is not a valid identifier: %1%"), cloud_uuid));
|
||||
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Cloud plugin UUID is not a valid identifier: %1%"), cloud_uuid));
|
||||
|
||||
const fs::path plugin_root = is_cloud_install && !cloud_user_id.empty() ? fs::path(get_cloud_plugin_dir(cloud_user_id)) :
|
||||
local_plugin_root();
|
||||
fs::create_directories(plugin_root, ec);
|
||||
if (ec)
|
||||
// TRN %1% is a folder path, %2% the system error message.
|
||||
return fail(GUI::format(_u8L("Failed to create plugin directory %1%: %2%"), plugin_root.string(), ec.message()));
|
||||
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Failed to create plugin directory %1%: %2%"), plugin_root.string(), ec.message()));
|
||||
|
||||
std::string meta_error;
|
||||
if (is_cloud_install) {
|
||||
@@ -718,7 +717,7 @@ bool install_plugin(const boost::filesystem::path& filepath,
|
||||
fs::rename(final_dir, backup_dir, ec);
|
||||
if (ec)
|
||||
// TRN %1% is a folder path, %2% the system error message.
|
||||
return fail(GUI::format(_u8L("Failed to back up existing plugin %1%: %2%"), final_dir.string(), ec.message()));
|
||||
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Failed to back up existing plugin %1%: %2%"), final_dir.string(), ec.message()));
|
||||
backup_created = true;
|
||||
}
|
||||
|
||||
@@ -729,7 +728,7 @@ bool install_plugin(const boost::filesystem::path& filepath,
|
||||
boost::system::error_code restore_ec;
|
||||
fs::rename(backup_dir, final_dir, restore_ec);
|
||||
}
|
||||
return fail(GUI::format(_u8L("Failed to create plugin directory %1%: %2%"), final_dir.string(), ec.message()));
|
||||
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Failed to create plugin directory %1%: %2%"), final_dir.string(), ec.message()));
|
||||
}
|
||||
|
||||
fs::copy_file(source_path, dest_file, ec);
|
||||
@@ -740,7 +739,7 @@ bool install_plugin(const boost::filesystem::path& filepath,
|
||||
fs::rename(backup_dir, final_dir, restore_ec);
|
||||
}
|
||||
// TRN %1% is a file path, %2% the system error message.
|
||||
return fail(GUI::format(_u8L("Failed to copy plugin file to %1%: %2%"), dest_file.string(), ec.message()));
|
||||
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Failed to copy plugin file to %1%: %2%"), dest_file.string(), ec.message()));
|
||||
}
|
||||
|
||||
// Update entry_path to the installed location.
|
||||
@@ -760,7 +759,7 @@ bool install_plugin(const boost::filesystem::path& filepath,
|
||||
fs::remove_all(final_dir, rollback_ec);
|
||||
if (backup_created)
|
||||
fs::rename(backup_dir, final_dir, rollback_ec);
|
||||
return fail(GUI::format(_u8L("Failed to write plugin install state: %1%"), (final_dir / INSTALL_STATE_FILE).string()));
|
||||
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Failed to write plugin install state: %1%"), (final_dir / INSTALL_STATE_FILE).string()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,18 +53,6 @@ const char* const LOAD_CANCELLED = "__plugin_load_cancelled__";
|
||||
|
||||
} // namespace
|
||||
|
||||
void Plugin::release_module()
|
||||
{
|
||||
if (module == nullptr && plugin_sys_paths.empty() && plugin_modules.empty())
|
||||
return;
|
||||
|
||||
PythonInterpreter::instance().unload_module(module, module_name, plugin_sys_paths, plugin_modules);
|
||||
module = nullptr;
|
||||
module_name.clear();
|
||||
plugin_sys_paths.clear();
|
||||
plugin_modules.clear();
|
||||
}
|
||||
|
||||
PluginManager& PluginManager::instance()
|
||||
{
|
||||
// Touch the interpreter first so its static outlives this one: ~Plugin needs it alive to
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Plugin module ownership is part of the loader's contract, not of the registry's GUI-facing
|
||||
// surface. Kept in its own translation unit so the headless probe can link the loader without
|
||||
// pulling PluginManager.cpp (and therefore wx/GUI) into the probe.
|
||||
|
||||
#include "PluginManager.hpp"
|
||||
#include "PythonInterpreter.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void Plugin::release_module()
|
||||
{
|
||||
if (module == nullptr && plugin_sys_paths.empty() && plugin_modules.empty())
|
||||
return;
|
||||
|
||||
PythonInterpreter::instance().unload_module(module, module_name, plugin_sys_paths, plugin_modules);
|
||||
module = nullptr;
|
||||
module_name.clear();
|
||||
plugin_sys_paths.clear();
|
||||
plugin_modules.clear();
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
// User-facing plugin messages are translated in the GUI build. The headless probe links neither
|
||||
// libslic3r_gui nor wx, so it formats the same English text without wxGetTranslation. The GUI path
|
||||
// expands to exactly the calls the plugin sources used before this header existed.
|
||||
|
||||
#if defined(ORCA_PLUGIN_PROBE_BUILD)
|
||||
|
||||
#include "libslic3r/format.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r::plugin_text {
|
||||
|
||||
inline std::string translate_utf8(const char* text) { return text == nullptr ? std::string{} : std::string(text); }
|
||||
inline std::string translate_utf8(const std::string& text) { return text; }
|
||||
|
||||
template<typename... TArgs>
|
||||
inline std::string format(const char* fmt, TArgs&&... args)
|
||||
{
|
||||
return Slic3r::format(fmt, std::forward<TArgs>(args)...);
|
||||
}
|
||||
|
||||
template<typename... TArgs>
|
||||
inline std::string format(const std::string& fmt, TArgs&&... args)
|
||||
{
|
||||
return Slic3r::format(fmt, std::forward<TArgs>(args)...);
|
||||
}
|
||||
|
||||
} // namespace Slic3r::plugin_text
|
||||
|
||||
#define PLUGIN_U8L(text) ::Slic3r::plugin_text::translate_utf8(text)
|
||||
#define PLUGIN_FORMAT(...) ::Slic3r::plugin_text::format(__VA_ARGS__)
|
||||
|
||||
#else
|
||||
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
|
||||
#define PLUGIN_U8L(text) _u8L(text)
|
||||
#define PLUGIN_FORMAT(...) GUI::format(__VA_ARGS__)
|
||||
|
||||
#endif
|
||||
@@ -30,7 +30,7 @@
|
||||
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
|
||||
#include <pybind11/pytypes.h>
|
||||
#include <pybind11/gil.h>
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "PluginText.hpp"
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
@@ -193,7 +193,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
|
||||
granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions);
|
||||
}
|
||||
if (!granted) {
|
||||
error = _u8L("Plugin filesystem read permission request denied");
|
||||
error = PLUGIN_U8L("Plugin filesystem read permission request denied");
|
||||
BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key;
|
||||
discard_pending_for_key();
|
||||
clear_active_key();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// libslic3r references NanoSVG without carrying its header-only implementation. This probe links
|
||||
// libslic3r without the GUI BitmapCache.cpp that supplies the implementation in the application.
|
||||
#define NANOSVG_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvg.h"
|
||||
@@ -0,0 +1,32 @@
|
||||
// Headless replacement for the GUI permission front end. The probe installs an observation
|
||||
// observer, so the audit hook records and returns before reaching this seam. If that observer is
|
||||
// ever absent, permission decisions fail closed instead of consulting GUI or install state.
|
||||
|
||||
#include "slic3r/plugin/PluginAuditManagerInternal.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r::plugin_audit {
|
||||
|
||||
bool request_filesystem_read_permissions(const std::string& plugin_key,
|
||||
const std::vector<std::string>& paths)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << "[PROBE] Refusing filesystem read permission request for " << plugin_key
|
||||
<< " (" << paths.size() << " path(s)): the headless probe does not evaluate grants";
|
||||
return false;
|
||||
}
|
||||
|
||||
int decide_event(PluginAuditManager& mgr,
|
||||
const std::string& event_name,
|
||||
AuditEventCategory category,
|
||||
const std::vector<std::string>& targets)
|
||||
{
|
||||
(void) category;
|
||||
(void) targets;
|
||||
return report_denied(mgr, event_name, {false, "permission decisions are unavailable in the headless probe"});
|
||||
}
|
||||
|
||||
} // namespace Slic3r::plugin_audit
|
||||
@@ -0,0 +1,75 @@
|
||||
// Probe-only `orca.host` registration. The GUI-free domains use the production bindings; the
|
||||
// domains that need a live GUI application, main window or preset bundle are exposed as explicit
|
||||
// unsupported submodules instead of being compiled (and linked) into the probe.
|
||||
//
|
||||
// A plugin that touches one of those domains fails its own callback with a clear message; the
|
||||
// failure is recorded by the probe's callback report rather than becoming a link dependency.
|
||||
|
||||
#include "slic3r/plugin/host/PluginHost.hpp"
|
||||
#include "slic3r/plugin/host/PluginHostBindings.hpp"
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/pytypes.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
void add_unavailable_domain(py::module_& host, const char* name, const char* description)
|
||||
{
|
||||
py::module_ domain = host.def_submodule(name, description);
|
||||
const std::string prefix = std::string("orca.host.") + name + ".";
|
||||
domain.attr("__getattr__") = py::cpp_function([prefix](const std::string& attribute) -> py::object {
|
||||
throw std::runtime_error(prefix + attribute +
|
||||
" is not available in the headless plugin permission probe because it needs the GUI application");
|
||||
});
|
||||
}
|
||||
|
||||
void add_unavailable_function(py::module_& host, const char* name)
|
||||
{
|
||||
// Production registers these in PluginHostApp::register_app(). Do not overwrite a slot that
|
||||
// another registrar already claimed (the model domain binds the Model class through
|
||||
// register_model); the probe only fills the GUI-backed accessors that are absent here.
|
||||
if (py::hasattr(host, name))
|
||||
return;
|
||||
|
||||
const std::string detail = std::string("orca.host.") + name +
|
||||
" is not available in the headless plugin permission probe because it needs the GUI application";
|
||||
host.def(name, [detail](py::args, py::kwargs) -> py::object { throw std::runtime_error(detail); }, detail.c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PluginHost::RegisterBindings(py::module_& module)
|
||||
{
|
||||
auto host = module.def_submodule("host", "Host application API");
|
||||
|
||||
// Value types first so the docstring signatures of later registrars resolve to the bound names.
|
||||
host_bindings::register_geometry(host);
|
||||
host_bindings::register_mesh(host);
|
||||
host_bindings::register_model(host);
|
||||
host_bindings::register_slicing(host);
|
||||
|
||||
// These domains are backed by the live GUI, its Plater/MainFrame and the preset bundle.
|
||||
add_unavailable_domain(host, "presets", "Preset bundle access (headless probe: unavailable)");
|
||||
add_unavailable_domain(host, "app", "Live application and Plater access (headless probe: unavailable)");
|
||||
add_unavailable_domain(host, "ui", "Native dialogs and HTML windows (headless probe: unavailable)");
|
||||
for (const char* name : {"plater", "model", "preset_bundle", "app_info", "gl_info", "selected_printer"})
|
||||
add_unavailable_function(host, name);
|
||||
|
||||
// Storage needs the PluginManager registry, which is not part of the headless probe.
|
||||
auto plugin_host = host.def_submodule("plugin", "Plugin host API (headless probe: storage unavailable)");
|
||||
plugin_host.def("storage", []() -> std::string {
|
||||
throw std::runtime_error("orca.host.plugin.storage() is not available in the headless plugin permission probe");
|
||||
}, "Return the installed folder of the current plugin.");
|
||||
plugin_host.attr("__getattr__") = py::cpp_function([](const std::string& attribute) -> py::object {
|
||||
throw std::runtime_error("orca.host.plugin." + attribute +
|
||||
" is not available in the headless plugin permission probe");
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,800 @@
|
||||
// Headless, record-only plugin permission probe.
|
||||
//
|
||||
// This is a developer/security-analysis tool, not a second production loader. It reuses the
|
||||
// embedded interpreter, the production plugin_loader sequence and the GUI-free `orca` bindings
|
||||
// (capability types plus the geometry/model/slicing host domains), so the observations come from
|
||||
// the same audit classification and target extraction as the GUI. It then lets every audited
|
||||
// operation continue instead of consulting permission grants or dialogs. Host domains that need
|
||||
// the live GUI (app, presets, ui) and plugin.storage are registered as explicit unsupported
|
||||
// errors; the config API reports an empty config and refuses writes.
|
||||
//
|
||||
// It is NOT a sandbox. Plugin code runs with the probe's filesystem, network and process
|
||||
// privileges. The temporary data directory only keeps OrcaSlicer's own state out of the user's
|
||||
// tree; it does not confine plugin-authored code. Use OS-level restrictions (container, user,
|
||||
// seccomp/AppArmor, firewall) when the package is not trusted.
|
||||
//
|
||||
// The report describes only the paths this run exercised. CPython audit events are observations of
|
||||
// particular operations, not a notification for every Python call, so absence of a category in the
|
||||
// report is not evidence that the plugin cannot perform it.
|
||||
//
|
||||
// The report separates package validation, runtime compatibility, callback results and observed
|
||||
// audit activity, then derives an advisory readiness status from them. Readiness never changes the
|
||||
// process exit status, which stays load-based for existing callers.
|
||||
|
||||
#include "slic3r/plugin/PluginAuditManager.hpp"
|
||||
#include "slic3r/plugin/PluginDescriptor.hpp"
|
||||
#include "slic3r/plugin/PluginFsUtils.hpp"
|
||||
#include "slic3r/plugin/PluginLoader.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "slic3r/plugin/PythonInterpreter.hpp"
|
||||
#include "slic3r/plugin/PythonPluginInterface.hpp"
|
||||
#include "slic3r/plugin/probe/PluginProbeCollector.hpp"
|
||||
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <pybind11/embed.h>
|
||||
#include <pybind11/gil.h>
|
||||
#include <pybind11/pytypes.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
namespace py = pybind11;
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kLoadPhase = "load:import+registration+on_load";
|
||||
|
||||
struct Options
|
||||
{
|
||||
fs::path plugin_path;
|
||||
bool json = false;
|
||||
};
|
||||
|
||||
void print_usage(std::ostream& out)
|
||||
{
|
||||
out << "Usage: orca-plugin-probe [--json] <plugin.py|plugin.whl>\n"
|
||||
"\n"
|
||||
"Runs a plugin in a temporary data/working directory and reports the CPython audit\n"
|
||||
"events observed while its import, registration, on_load and discoverable callbacks\n"
|
||||
"execute. The probe does not prompt for permissions, write grants, install dependencies,\n"
|
||||
"or start the OrcaSlicer GUI. The report summarizes package validation, runtime\n"
|
||||
"compatibility, callback results and audit activity, plus an advisory readiness status.\n"
|
||||
"\n"
|
||||
" --json Emit the report as JSON (default: human-readable text)\n"
|
||||
" --help Show this message\n"
|
||||
"\n"
|
||||
"WARNING: this is not a sandbox. Plugin Python code runs with this process's privileges.\n"
|
||||
"Results are observational and non-exhaustive. Use OS-level restrictions for untrusted\n"
|
||||
"packages.\n"
|
||||
"\n"
|
||||
"Exit status: 0 plugin loaded, 1 initialization/load failure, 2 usage error.\n";
|
||||
}
|
||||
|
||||
bool parse_options(int argc, char** argv, Options& options, std::string& error)
|
||||
{
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string argument = argv[index];
|
||||
if (argument == "--help" || argument == "-h") {
|
||||
print_usage(std::cout);
|
||||
std::exit(0);
|
||||
}
|
||||
if (argument == "--json") {
|
||||
options.json = true;
|
||||
continue;
|
||||
}
|
||||
if (!argument.empty() && argument.front() == '-') {
|
||||
error = "Unknown option: " + argument;
|
||||
return false;
|
||||
}
|
||||
if (!options.plugin_path.empty()) {
|
||||
error = "Only one plugin package may be probed at a time.";
|
||||
return false;
|
||||
}
|
||||
options.plugin_path = fs::path(argument);
|
||||
}
|
||||
|
||||
if (options.plugin_path.empty()) {
|
||||
error = "No plugin package given.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keep every write OrcaSlicer makes during the run inside a throwaway tree, and keep relative
|
||||
// paths opened by plugin code out of the caller's working directory. This is hygiene, not
|
||||
// confinement: an absolute path supplied by the plugin is still opened normally.
|
||||
class ScopedProbeWorkspace
|
||||
{
|
||||
public:
|
||||
ScopedProbeWorkspace()
|
||||
: m_root(fs::temp_directory_path() / fs::unique_path("orca-plugin-probe-%%%%-%%%%-%%%%"))
|
||||
, m_previous_data_dir(data_dir())
|
||||
, m_previous_working_dir(fs::current_path())
|
||||
{
|
||||
fs::create_directories(m_root);
|
||||
set_data_dir(m_root.string());
|
||||
fs::current_path(m_root);
|
||||
m_moved = true;
|
||||
}
|
||||
|
||||
~ScopedProbeWorkspace()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
set_data_dir(m_previous_data_dir);
|
||||
if (m_moved)
|
||||
fs::current_path(m_previous_working_dir, ec);
|
||||
fs::remove_all(m_root, ec);
|
||||
}
|
||||
|
||||
const fs::path& root() const { return m_root; }
|
||||
|
||||
private:
|
||||
fs::path m_root;
|
||||
std::string m_previous_data_dir;
|
||||
fs::path m_previous_working_dir;
|
||||
bool m_moved = false;
|
||||
};
|
||||
|
||||
class ScopedObservationObserver
|
||||
{
|
||||
public:
|
||||
explicit ScopedObservationObserver(PluginProbeCollector& collector) : m_collector(collector)
|
||||
{
|
||||
PluginAuditManager::instance().set_observation_observer(
|
||||
[this](const AuditObservation& observation) { m_collector.record(observation); });
|
||||
}
|
||||
|
||||
~ScopedObservationObserver() { PluginAuditManager::instance().clear_observation_observer(); }
|
||||
|
||||
ScopedObservationObserver(const ScopedObservationObserver&) = delete;
|
||||
ScopedObservationObserver& operator=(const ScopedObservationObserver&) = delete;
|
||||
|
||||
private:
|
||||
PluginProbeCollector& m_collector;
|
||||
};
|
||||
|
||||
bool prepare_descriptor(const fs::path& plugin_path, PluginDescriptor& descriptor, std::string& error)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
const fs::path canonical = fs::weakly_canonical(plugin_path, ec);
|
||||
if (ec || !fs::exists(canonical) || !fs::is_regular_file(canonical)) {
|
||||
error = "Plugin package is not a readable file: " + plugin_path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string extension = canonical.extension().string();
|
||||
boost::algorithm::to_lower(extension);
|
||||
if (extension != ".py" && extension != ".whl") {
|
||||
error = "Plugin package must be a .py or .whl file, got: " + extension;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extension == ".whl") {
|
||||
if (!read_wheel_plugin_metadata(canonical, descriptor, error))
|
||||
return false;
|
||||
} else {
|
||||
if (!read_python_plugin_metadata(canonical, descriptor, error))
|
||||
return false;
|
||||
}
|
||||
|
||||
descriptor.entry_path = canonical.string();
|
||||
descriptor.plugin_key = make_local_plugin_key(canonical.stem().string());
|
||||
descriptor.metadata_valid = true;
|
||||
// Leaving plugin_root empty keeps the probe independent of any .install_state.json next to
|
||||
// the package and avoids the production sidecar path.
|
||||
descriptor.plugin_root.clear();
|
||||
if (descriptor.name.empty())
|
||||
descriptor.name = descriptor.plugin_key;
|
||||
return true;
|
||||
}
|
||||
|
||||
void redirect_python_stdout()
|
||||
{
|
||||
py::gil_scoped_acquire gil;
|
||||
py::object sys = py::module_::import("sys");
|
||||
// Keep plugin prints from corrupting the C++ report on stdout. sys.stderr is the interpreter's
|
||||
// tee to the temporary log, so plugin output remains available there.
|
||||
sys.attr("stdout") = sys.attr("stderr");
|
||||
}
|
||||
|
||||
// Captured while the interpreter is still alive. Best effort: a failed lookup leaves the field
|
||||
// empty and does not overwrite the initialization error.
|
||||
std::string query_python_version()
|
||||
{
|
||||
py::gil_scoped_acquire gil;
|
||||
try {
|
||||
return py::module_::import("platform").attr("python_version")().cast<std::string>();
|
||||
} catch (const std::exception&) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::string package_format(const fs::path& path)
|
||||
{
|
||||
std::string extension = path.extension().string();
|
||||
boost::algorithm::to_lower(extension);
|
||||
if (extension == ".whl")
|
||||
return "wheel";
|
||||
if (extension == ".py")
|
||||
return "python-script";
|
||||
return extension.empty() ? "unknown" : extension;
|
||||
}
|
||||
|
||||
template<class Function>
|
||||
void invoke_callback(const std::shared_ptr<PluginCapabilityInterface>& capability,
|
||||
const std::string& phase,
|
||||
const std::string& callback,
|
||||
PluginProbeCollector& collector,
|
||||
Function&& function)
|
||||
{
|
||||
ScopedPluginAuditPhase phase_scope(phase);
|
||||
try {
|
||||
std::forward<Function>(function)();
|
||||
collector.record_callback({capability ? capability->name() : std::string{}, phase, callback, "invoked", {}});
|
||||
} catch (const std::exception& error) {
|
||||
collector.record_callback({capability ? capability->name() : std::string{}, phase, callback, "failed", error.what()});
|
||||
} catch (...) {
|
||||
collector.record_callback({capability ? capability->name() : std::string{}, phase, callback, "failed", "non-standard exception"});
|
||||
}
|
||||
}
|
||||
|
||||
struct ProbeCapability
|
||||
{
|
||||
std::string name;
|
||||
std::string type;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
std::vector<ProbeCapability> describe_capabilities(const Plugin& plugin)
|
||||
{
|
||||
std::vector<ProbeCapability> capabilities;
|
||||
capabilities.reserve(plugin.capabilities.size());
|
||||
for (const auto& capability : plugin.capabilities) {
|
||||
if (!capability)
|
||||
continue;
|
||||
capabilities.push_back({capability->name(), plugin_capability_type_to_string(capability->type()), capability->is_enabled()});
|
||||
}
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
void exercise_callbacks(const Plugin& plugin, PluginProbeCollector& collector)
|
||||
{
|
||||
// on_load() already ran inside plugin_loader::load. The callbacks below are the additional
|
||||
// entry points a loaded capability exposes to the host. Synthetic inputs are used; a callback
|
||||
// may behave differently with a real application state, which the report cannot capture.
|
||||
static constexpr LifecycleEvent lifecycle_events[] = {
|
||||
LifecycleEvent::NewProject, LifecycleEvent::ProjectOpened, LifecycleEvent::ProjectBeforeSave,
|
||||
LifecycleEvent::ProjectAfterSave, LifecycleEvent::ProjectClosed, LifecycleEvent::ProjectDirtyChanged,
|
||||
LifecycleEvent::SliceStarted, LifecycleEvent::SliceGeometryFinished, LifecycleEvent::GCodeExportStarted,
|
||||
LifecycleEvent::GCodeExportFinished, LifecycleEvent::SlicingJobComplete, LifecycleEvent::ObjectAdded,
|
||||
LifecycleEvent::ObjectDeleted, LifecycleEvent::ObjectTransformed, LifecycleEvent::ObjectChanged,
|
||||
LifecycleEvent::ObjectRenamed, LifecycleEvent::PlateCreated, LifecycleEvent::PlateDeleted,
|
||||
LifecycleEvent::PlateSelected, LifecycleEvent::PlateRenamed, LifecycleEvent::PresetSelected,
|
||||
LifecycleEvent::PresetSaved, LifecycleEvent::PrintStateChanged, LifecycleEvent::DeviceOnline,
|
||||
LifecycleEvent::DeviceOffline, LifecycleEvent::DeviceDiscovered, LifecycleEvent::DeviceSelected,
|
||||
LifecycleEvent::UploadStarted, LifecycleEvent::UploadFinished, LifecycleEvent::PrintJobStarted,
|
||||
LifecycleEvent::PrintJobFinished, LifecycleEvent::SendJobStarted, LifecycleEvent::SendJobFinished,
|
||||
};
|
||||
|
||||
for (const auto& capability : plugin.capabilities) {
|
||||
if (!capability)
|
||||
continue;
|
||||
|
||||
invoke_callback(capability, "callback:get_default_config", "get_default_config", collector,
|
||||
[&capability]() { (void) capability->get_default_config(); });
|
||||
|
||||
if (capability->config_ui_available()) {
|
||||
invoke_callback(capability, "callback:get_config_ui", "get_config_ui", collector,
|
||||
[&capability]() { (void) capability->get_config_ui(); });
|
||||
} else {
|
||||
collector.record_callback(
|
||||
{capability->name(), "callback:get_config_ui", "get_config_ui", "skipped", "has_config_ui() is false"});
|
||||
}
|
||||
|
||||
for (const LifecycleEvent event : lifecycle_events) {
|
||||
const std::string event_name = lifecycle_event_to_string(event);
|
||||
const std::string phase = "callback:on_lifecycle_event(" + event_name + ")";
|
||||
LifecycleEventContext context;
|
||||
context.source = "orca-plugin-probe";
|
||||
context.msg = "synthetic event generated by the permission probe";
|
||||
invoke_callback(capability, phase, "on_lifecycle_event(" + event_name + ")", collector,
|
||||
[&capability, event, &context]() { capability->on_lifecycle_event(event, context); });
|
||||
}
|
||||
|
||||
// Cancellation is a load-time path; calling it after a successful load would misrepresent
|
||||
// the capability's real lifecycle. Report it as skipped instead.
|
||||
collector.record_callback(
|
||||
{capability->name(), "callback:on_cancelled", "on_cancelled", "skipped", "not invoked after a successful load"});
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json observation_json(const AuditObservation& observation)
|
||||
{
|
||||
return {
|
||||
{"plugin", observation.plugin_key},
|
||||
{"capability", observation.capability},
|
||||
{"phase", observation.phase.empty() ? "unattributed" : observation.phase},
|
||||
{"event", observation.event_name},
|
||||
{"category", observation.category},
|
||||
{"targets", observation.targets},
|
||||
{"continued", observation.continued},
|
||||
};
|
||||
}
|
||||
|
||||
nlohmann::json callback_json(const ProbeCallbackRecord& callback)
|
||||
{
|
||||
return {
|
||||
{"capability", callback.capability}, {"phase", callback.phase}, {"callback", callback.callback},
|
||||
{"status", callback.status}, {"detail", callback.detail},
|
||||
};
|
||||
}
|
||||
|
||||
nlohmann::json capability_json(const ProbeCapability& capability)
|
||||
{
|
||||
return {
|
||||
{"name", capability.name},
|
||||
{"type", capability.type},
|
||||
{"enabled", capability.enabled},
|
||||
};
|
||||
}
|
||||
|
||||
nlohmann::json build_report(const Options& options,
|
||||
const PluginProbeCollector& collector,
|
||||
const std::vector<ProbeCapability>& capabilities,
|
||||
const std::string& plugin_key,
|
||||
const PluginDescriptor& descriptor,
|
||||
bool descriptor_ready,
|
||||
const std::string& descriptor_error,
|
||||
bool python_initialized,
|
||||
const std::string& python_version,
|
||||
const std::string& python_error,
|
||||
bool loaded,
|
||||
const std::string& load_error,
|
||||
const std::string& data_dir)
|
||||
{
|
||||
const std::vector<AuditObservation> observed_events = collector.observations();
|
||||
nlohmann::json observations = nlohmann::json::array();
|
||||
std::size_t unattributed_events = 0;
|
||||
std::size_t non_continued_events = 0;
|
||||
for (const AuditObservation& observation : observed_events) {
|
||||
observations.push_back(observation_json(observation));
|
||||
if (observation.phase.empty())
|
||||
++unattributed_events;
|
||||
if (!observation.continued)
|
||||
++non_continued_events;
|
||||
}
|
||||
|
||||
const std::vector<ProbeCallbackRecord> callback_records = collector.callbacks();
|
||||
nlohmann::json callbacks = nlohmann::json::array();
|
||||
std::size_t invoked_callbacks = 0;
|
||||
std::size_t failed_callbacks = 0;
|
||||
std::size_t skipped_callbacks = 0;
|
||||
for (const ProbeCallbackRecord& callback : callback_records) {
|
||||
callbacks.push_back(callback_json(callback));
|
||||
if (callback.status == "invoked")
|
||||
++invoked_callbacks;
|
||||
else if (callback.status == "failed")
|
||||
++failed_callbacks;
|
||||
else if (callback.status == "skipped")
|
||||
++skipped_callbacks;
|
||||
}
|
||||
|
||||
nlohmann::json capability_list = nlohmann::json::array();
|
||||
for (const ProbeCapability& capability : capabilities)
|
||||
capability_list.push_back(capability_json(capability));
|
||||
|
||||
const nlohmann::json summary = collector.summary();
|
||||
const bool package_valid = descriptor_ready && descriptor.metadata_valid;
|
||||
const std::string entry_path = descriptor.entry_path.empty() ? options.plugin_path.string() : descriptor.entry_path;
|
||||
const std::string observed_plugin = plugin_key.empty() ? make_local_plugin_key(options.plugin_path.stem().string()) : plugin_key;
|
||||
|
||||
nlohmann::json readiness_issues = nlohmann::json::array();
|
||||
if (!descriptor_ready) {
|
||||
readiness_issues.push_back({{"severity", "blocking"}, {"detail", "package validation failed: " + descriptor_error}});
|
||||
} else if (!python_initialized) {
|
||||
readiness_issues.push_back(
|
||||
{{"severity", "blocking"},
|
||||
{"detail", "Python interpreter initialization failed" + (python_error.empty() ? std::string{} : ": " + python_error)}});
|
||||
} else if (!loaded) {
|
||||
readiness_issues.push_back({{"severity", "blocking"},
|
||||
{"detail", "plugin_loader::load failed" + (load_error.empty() ? std::string{} : ": " + load_error)}});
|
||||
}
|
||||
if (package_valid && python_initialized && loaded && failed_callbacks > 0)
|
||||
readiness_issues.push_back(
|
||||
{{"severity", "warning"},
|
||||
{"detail", std::to_string(failed_callbacks) + " callback(s) failed; see callbacks for the individual records"}});
|
||||
|
||||
const char* readiness_status = (!package_valid || !python_initialized || !loaded) ? "not_ready" :
|
||||
failed_callbacks > 0 ? "degraded" :
|
||||
"ready";
|
||||
|
||||
return {
|
||||
{"tool", "orca-plugin-permission-probe"},
|
||||
{"mode", "record-only"},
|
||||
{"observed_by_this_run_only", true},
|
||||
{"plugin",
|
||||
{
|
||||
{"path", options.plugin_path.string()},
|
||||
{"key", observed_plugin},
|
||||
}},
|
||||
{"readiness",
|
||||
{
|
||||
{"status", readiness_status},
|
||||
{"advisory", true},
|
||||
{"checks",
|
||||
{
|
||||
{"package_valid", package_valid},
|
||||
{"python_initialized", python_initialized},
|
||||
{"load_ok", loaded},
|
||||
{"failed_callback_count", failed_callbacks},
|
||||
}},
|
||||
{"issues", readiness_issues},
|
||||
{"exit_status",
|
||||
{
|
||||
{"code", loaded ? 0 : 1},
|
||||
{"semantics", "0 when the plugin loaded, 1 on package validation, initialization or load failure, 2 on a usage error"},
|
||||
{"driven_by", "load"},
|
||||
{"note", "Readiness is advisory and does not change the exit status; degraded still exits 0."},
|
||||
}},
|
||||
}},
|
||||
{"package",
|
||||
{
|
||||
{"valid", package_valid},
|
||||
{"metadata_read", descriptor_ready},
|
||||
{"entry_path", entry_path},
|
||||
{"entry_format", package_format(fs::path(entry_path))},
|
||||
{"metadata",
|
||||
{
|
||||
{"name", descriptor.name},
|
||||
{"version", descriptor.version},
|
||||
{"author", descriptor.author},
|
||||
{"description", descriptor.description},
|
||||
{"entry_package", descriptor.entry_package},
|
||||
}},
|
||||
{"declared_dependencies", descriptor.dependencies},
|
||||
{"error", descriptor_ready ? std::string{} : descriptor_error},
|
||||
{"installation",
|
||||
{
|
||||
{"performed", false},
|
||||
{"dependencies_installed", false},
|
||||
{"note", "The probe inspects the given package in place; it does not install the package or its dependencies."},
|
||||
}},
|
||||
}},
|
||||
{"environment",
|
||||
{
|
||||
{"python_initialized", python_initialized},
|
||||
{"python_error", python_error},
|
||||
{"temporary_data_dir", data_dir},
|
||||
{"temporary_data_dir_removed_on_exit", true},
|
||||
{"working_directory", data_dir},
|
||||
{"interactive_prompts", false},
|
||||
{"grants_persisted", false},
|
||||
}},
|
||||
{"runtime",
|
||||
{
|
||||
{"python",
|
||||
{
|
||||
{"initialized", python_initialized},
|
||||
{"version", python_version},
|
||||
}},
|
||||
{"headless", true},
|
||||
{"gui_initialized", false},
|
||||
{"unavailable_gui_domains", nlohmann::json::array({"orca.host.app", "orca.host.presets", "orca.host.ui", "orca.host.plugin"})},
|
||||
{"gui_domain_behavior",
|
||||
"Attribute access raises an unsupported error; the probe records it as a failed callback when plugin code "
|
||||
"touches the domain."},
|
||||
{"config_api",
|
||||
{
|
||||
{"get_config", "empty object"},
|
||||
{"get_config_version", "empty string"},
|
||||
{"save_config", "refused"},
|
||||
{"persisted", false},
|
||||
}},
|
||||
{"storage_api",
|
||||
{
|
||||
{"available", false},
|
||||
{"detail", "orca.host.plugin.storage() raises an unsupported error because the probe does not start the "
|
||||
"PluginManager registry."},
|
||||
}},
|
||||
}},
|
||||
{"load", {{"ok", loaded}, {"error", load_error}, {"capabilities", capability_list}}},
|
||||
{"callback_results",
|
||||
{
|
||||
{"records_section", "callbacks"},
|
||||
{"total", callback_records.size()},
|
||||
{"invoked", invoked_callbacks},
|
||||
{"failed", failed_callbacks},
|
||||
{"skipped", skipped_callbacks},
|
||||
{"note", "The callbacks array holds the individual records; this section summarizes them and feeds readiness."},
|
||||
}},
|
||||
{"callbacks", callbacks},
|
||||
{"audit_activity",
|
||||
{
|
||||
{"records_section", "observations"},
|
||||
{"summary_section", "summary"},
|
||||
{"observed_event_count", observed_events.size()},
|
||||
{"category_count", summary["categories"].size()},
|
||||
{"unattributed_event_count", unattributed_events},
|
||||
{"non_continued_event_count", non_continued_events},
|
||||
{"side_effects",
|
||||
{
|
||||
{"plugin_code_executed", true},
|
||||
{"installation_performed", false},
|
||||
{"dependencies_installed", false},
|
||||
{"grants_persisted", false},
|
||||
{"filesystem_network_process_contained", false},
|
||||
{"temporary_data_dir_removed_on_exit", true},
|
||||
}},
|
||||
{"note", "Side effects are the probe's own actions plus what the audit hook observed; only exercised paths are "
|
||||
"represented."},
|
||||
}},
|
||||
{"observations", observations},
|
||||
{"summary", summary},
|
||||
{"coverage",
|
||||
{
|
||||
{"exhaustive", false},
|
||||
{"sandbox", false},
|
||||
{"plugin_code_executed", true},
|
||||
{"notes",
|
||||
{
|
||||
"Only the code paths this probe exercised are represented. Audit events cover "
|
||||
"particular operations, not every Python call.",
|
||||
"The probe does not start the GUI. Host APIs that require a live application or "
|
||||
"UI (orca.host.app/presets/ui) and plugin.storage report an explicit unsupported "
|
||||
"error and are reported as failed callbacks.",
|
||||
"The probe links libslic3r for core geometry/print types, but not libslic3r_gui "
|
||||
"or wx.",
|
||||
"The temporary data directory isolates OrcaSlicer state, not plugin-authored file, "
|
||||
"network or process access. Use OS-level restrictions for untrusted packages.",
|
||||
"The loader may extract adjacent .whl dependencies into the package's directory; "
|
||||
"probe a copy of the package when that side effect is unwanted.",
|
||||
"Import, registration and on_load share one loader phase; the production loader "
|
||||
"does not expose a finer boundary without changing its behavior.",
|
||||
"Callbacks run with synthetic inputs, which can change plugin control flow "
|
||||
"compared with a real application session.",
|
||||
"continued=true means the audit hook did not block the operation. A surrounding "
|
||||
"callback or load step can still fail afterwards, as reported in callbacks/load.",
|
||||
"Events raised on a plugin-spawned thread carry no phase label and are reported "
|
||||
"as unattributed.",
|
||||
"Plugin code can spawn threads/processes, write anywhere this process can, or "
|
||||
"terminate the process. None of that is contained by the probe.",
|
||||
}},
|
||||
}},
|
||||
};
|
||||
}
|
||||
|
||||
void print_text_report(const nlohmann::json& report)
|
||||
{
|
||||
const auto or_none = [](const std::string& value) { return value.empty() ? std::string("(none)") : value; };
|
||||
|
||||
std::cout << "OrcaSlicer plugin readiness report (record-only)\n"
|
||||
"WARNING: observational only; not exhaustive and not a sandbox.\n\n";
|
||||
std::cout << "Plugin: " << report["plugin"]["path"].get<std::string>() << "\n";
|
||||
std::cout << "Readiness: " << report["readiness"]["status"].get<std::string>() << " (advisory; exit status follows the load result)\n";
|
||||
for (const auto& issue : report["readiness"]["issues"])
|
||||
std::cout << " " << issue["severity"].get<std::string>() << ": " << issue["detail"].get<std::string>() << "\n";
|
||||
std::cout << "Python initialized: " << (report["environment"]["python_initialized"].get<bool>() ? "yes" : "no") << "\n";
|
||||
if (!report["environment"]["python_error"].get<std::string>().empty())
|
||||
std::cout << "Python error: " << report["environment"]["python_error"].get<std::string>() << "\n";
|
||||
std::cout << "Load: " << (report["load"]["ok"].get<bool>() ? "ok" : "failed") << "\n";
|
||||
if (!report["load"]["error"].get<std::string>().empty())
|
||||
std::cout << "Load error: " << report["load"]["error"].get<std::string>() << "\n";
|
||||
|
||||
const auto& package = report["package"];
|
||||
std::cout << "\nPackage: valid=" << (package["valid"].get<bool>() ? "yes" : "no")
|
||||
<< " format=" << package["entry_format"].get<std::string>() << "\n";
|
||||
std::cout << " entry: " << package["entry_path"].get<std::string>() << "\n";
|
||||
std::cout << " name=" << or_none(package["metadata"]["name"].get<std::string>())
|
||||
<< " version=" << or_none(package["metadata"]["version"].get<std::string>())
|
||||
<< " author=" << or_none(package["metadata"]["author"].get<std::string>()) << "\n";
|
||||
if (!package["metadata"]["entry_package"].get<std::string>().empty())
|
||||
std::cout << " entry package: " << package["metadata"]["entry_package"].get<std::string>() << "\n";
|
||||
std::cout << " declared dependencies:";
|
||||
if (package["declared_dependencies"].empty()) {
|
||||
std::cout << " (none)";
|
||||
} else {
|
||||
bool first = true;
|
||||
for (const auto& dependency : package["declared_dependencies"]) {
|
||||
if (!first)
|
||||
std::cout << ", ";
|
||||
std::cout << dependency.get<std::string>();
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
std::cout << "\n installed: no; the probe does not install the package or its dependencies\n";
|
||||
if (!package["error"].get<std::string>().empty())
|
||||
std::cout << " package error: " << package["error"].get<std::string>() << "\n";
|
||||
|
||||
const auto& runtime = report["runtime"];
|
||||
std::cout << "\nRuntime: python_version=" << or_none(runtime["python"]["version"].get<std::string>())
|
||||
<< " headless=yes gui=not_started\n";
|
||||
std::cout << " unavailable GUI domains:";
|
||||
{
|
||||
bool first = true;
|
||||
for (const auto& domain : runtime["unavailable_gui_domains"]) {
|
||||
if (!first)
|
||||
std::cout << ", ";
|
||||
std::cout << domain.get<std::string>();
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
std::cout << "\n config API: returns an empty object, saves are refused, nothing persisted\n";
|
||||
std::cout << " storage API: unavailable (orca.host.plugin.storage raises)\n";
|
||||
|
||||
std::cout << "\nCapabilities (" << report["load"]["capabilities"].size() << "):\n";
|
||||
for (const auto& capability : report["load"]["capabilities"])
|
||||
std::cout << " " << capability["name"].get<std::string>() << " [" << capability["type"].get<std::string>()
|
||||
<< "] enabled=" << (capability["enabled"].get<bool>() ? "yes" : "no") << "\n";
|
||||
|
||||
const auto& callback_results = report["callback_results"];
|
||||
std::cout << "\nCallbacks (" << callback_results["total"].get<std::size_t>() << ": " << callback_results["invoked"].get<std::size_t>()
|
||||
<< " invoked, " << callback_results["failed"].get<std::size_t>() << " failed, "
|
||||
<< callback_results["skipped"].get<std::size_t>() << " skipped)\n";
|
||||
for (const auto& callback : report["callbacks"]) {
|
||||
std::cout << " " << callback["status"].get<std::string>() << " " << callback["callback"].get<std::string>();
|
||||
if (!callback["capability"].get<std::string>().empty())
|
||||
std::cout << " capability=" << callback["capability"].get<std::string>();
|
||||
if (!callback["detail"].get<std::string>().empty())
|
||||
std::cout << " - " << callback["detail"].get<std::string>();
|
||||
std::cout << "\n";
|
||||
}
|
||||
|
||||
std::cout << "\nObserved audit events (" << report["audit_activity"]["observed_event_count"].get<std::size_t>() << " events, "
|
||||
<< report["audit_activity"]["category_count"].get<std::size_t>() << " categories):\n";
|
||||
for (const auto& observation : report["observations"]) {
|
||||
std::cout << " phase=" << observation["phase"].get<std::string>() << " event=" << observation["event"].get<std::string>()
|
||||
<< " category=" << observation["category"].get<std::string>();
|
||||
if (!observation["capability"].get<std::string>().empty())
|
||||
std::cout << " capability=" << observation["capability"].get<std::string>();
|
||||
if (!observation["targets"].empty()) {
|
||||
std::cout << " targets=";
|
||||
bool first = true;
|
||||
for (const auto& target : observation["targets"]) {
|
||||
if (!first)
|
||||
std::cout << ",";
|
||||
std::cout << target.get<std::string>();
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
std::cout << " continued=" << (observation["continued"].get<bool>() ? "yes" : "no") << "\n";
|
||||
}
|
||||
|
||||
std::cout << "\nObserved permission categories:\n";
|
||||
for (const auto& [category, detail] : report["summary"]["categories"].items()) {
|
||||
std::cout << " " << category << ":";
|
||||
for (const auto& event : detail["events"])
|
||||
std::cout << " " << event.get<std::string>();
|
||||
if (!detail["targets"].empty()) {
|
||||
std::cout << " -> ";
|
||||
bool first = true;
|
||||
for (const auto& target : detail["targets"]) {
|
||||
if (!first)
|
||||
std::cout << ", ";
|
||||
std::cout << target.get<std::string>();
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
std::cout << "\n";
|
||||
}
|
||||
if (report["summary"]["categories"].empty())
|
||||
std::cout << " (none observed)\n";
|
||||
|
||||
std::cout << "\nObserved side effects: plugin code executed; package and dependencies not installed; no grants persisted; "
|
||||
"filesystem, network and process access not contained\n";
|
||||
std::cout << "Exit status: " << report["readiness"]["exit_status"]["code"].get<int>() << " (driven by load; readiness is advisory)\n";
|
||||
|
||||
std::cout << "\nCoverage limits:\n";
|
||||
for (const auto& note : report["coverage"]["notes"])
|
||||
std::cout << " - " << note.get<std::string>() << "\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
Options options;
|
||||
std::string option_error;
|
||||
if (!parse_options(argc, argv, options, option_error)) {
|
||||
std::cerr << "orca-plugin-probe: " << option_error << "\n\n";
|
||||
print_usage(std::cerr);
|
||||
return 2;
|
||||
}
|
||||
|
||||
boost::system::error_code ec;
|
||||
const fs::path canonical_plugin = fs::weakly_canonical(options.plugin_path, ec);
|
||||
if (!ec)
|
||||
options.plugin_path = canonical_plugin;
|
||||
|
||||
ScopedProbeWorkspace workspace;
|
||||
PluginProbeCollector collector;
|
||||
ScopedObservationObserver observer(collector);
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
std::string descriptor_error;
|
||||
const bool descriptor_ready = prepare_descriptor(options.plugin_path, descriptor, descriptor_error);
|
||||
|
||||
PythonInterpreter& interpreter = PythonInterpreter::instance();
|
||||
bool python_initialized = false;
|
||||
std::string python_error;
|
||||
if (descriptor_ready) {
|
||||
python_initialized = interpreter.initialize();
|
||||
if (!python_initialized)
|
||||
python_error = interpreter.last_error();
|
||||
}
|
||||
|
||||
if (python_initialized) {
|
||||
try {
|
||||
redirect_python_stdout();
|
||||
} catch (const std::exception& error) {
|
||||
python_error = std::string("Could not redirect Python stdout: ") + error.what();
|
||||
}
|
||||
}
|
||||
|
||||
std::string python_version;
|
||||
if (python_initialized)
|
||||
python_version = query_python_version();
|
||||
|
||||
bool loaded = false;
|
||||
std::string load_error = descriptor_error;
|
||||
Plugin plugin;
|
||||
std::vector<ProbeCapability> capabilities;
|
||||
|
||||
if (python_initialized && descriptor_ready) {
|
||||
// The outer context covers import, registration and on_load in plugin_loader::load; the
|
||||
// per-callback scopes below cover the follow-up calls. The production loader is unchanged.
|
||||
ScopedPluginAuditContext audit_context(descriptor.plugin_key);
|
||||
{
|
||||
ScopedPluginAuditPhase load_phase(kLoadPhase);
|
||||
loaded = plugin_loader::load(descriptor, /*skip_deps=*/true, /*capabilities_to_enable=*/{}, nullptr, plugin, load_error);
|
||||
collector.record_callback({std::string{}, kLoadPhase, "plugin_loader::load", loaded ? "invoked" : "failed", load_error});
|
||||
collector.record_callback({std::string{}, "callback:on_load", "on_load", loaded ? "invoked" : "skipped",
|
||||
loaded ? "invoked by plugin_loader::load for each materialized capability" :
|
||||
"plugin_loader::load failed before completing on_load"});
|
||||
}
|
||||
|
||||
capabilities = describe_capabilities(plugin);
|
||||
|
||||
if (loaded)
|
||||
exercise_callbacks(plugin, collector);
|
||||
|
||||
if (loaded) {
|
||||
ScopedPluginAuditPhase unload_phase("callback:on_unload");
|
||||
plugin_loader::unload(plugin);
|
||||
collector.record_callback({std::string{}, "callback:on_unload", "on_unload", "invoked",
|
||||
"invoked by plugin_loader::unload; per-capability failures are logged by the loader"});
|
||||
}
|
||||
}
|
||||
|
||||
if (interpreter.is_initialized())
|
||||
interpreter.shutdown();
|
||||
|
||||
const nlohmann::json report = build_report(options, collector, capabilities,
|
||||
descriptor.plugin_key.empty() ? make_local_plugin_key(options.plugin_path.stem().string()) :
|
||||
descriptor.plugin_key,
|
||||
descriptor, descriptor_ready, descriptor_error, python_initialized, python_version,
|
||||
python_error, loaded, load_error, workspace.root().string());
|
||||
|
||||
if (options.json)
|
||||
std::cout << report.dump(2, ' ', false, nlohmann::json::error_handler_t::replace) << "\n";
|
||||
else
|
||||
print_text_report(report);
|
||||
|
||||
return loaded ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#ifndef slic3r_PluginProbeCollector_hpp_
|
||||
#define slic3r_PluginProbeCollector_hpp_
|
||||
|
||||
// Record-only collection for the headless plugin permission probe. This is deliberately a plain
|
||||
// in-memory store: it has no dialog, grant or persistence path, and it never decides whether an
|
||||
// audited operation may proceed. Keeping it independent of wx/Python also makes its grouping and
|
||||
// phase attribution testable without starting the interpreter.
|
||||
|
||||
#include "slic3r/plugin/PluginAuditManager.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// What the probe attempted and what came back. `status` is one of "invoked", "failed" or
|
||||
// "skipped"; a skipped callback is reported so the absence of observations is not mistaken for a
|
||||
// clean result.
|
||||
struct ProbeCallbackRecord
|
||||
{
|
||||
std::string capability;
|
||||
std::string phase;
|
||||
std::string callback;
|
||||
std::string status;
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
class PluginProbeCollector
|
||||
{
|
||||
public:
|
||||
void record(const AuditObservation& observation)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_observations.push_back(observation);
|
||||
}
|
||||
|
||||
void record_callback(ProbeCallbackRecord record)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_callbacks.push_back(std::move(record));
|
||||
}
|
||||
|
||||
std::vector<AuditObservation> observations() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_observations;
|
||||
}
|
||||
|
||||
std::vector<ProbeCallbackRecord> callbacks() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_callbacks;
|
||||
}
|
||||
|
||||
// Groups the run by the permission categories the audit hook already assigned, and lists the
|
||||
// distinct targets and event names seen in each. This is a view of this run only; it says
|
||||
// nothing about code paths the plugin did not exercise.
|
||||
nlohmann::json summary() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
std::map<std::string, std::set<std::string>> events;
|
||||
std::map<std::string, std::set<std::string>> targets;
|
||||
for (const AuditObservation& observation : m_observations) {
|
||||
events[observation.category].insert(observation.event_name);
|
||||
targets[observation.category].insert(observation.targets.begin(), observation.targets.end());
|
||||
}
|
||||
|
||||
nlohmann::json categories = nlohmann::json::object();
|
||||
for (const auto& [category, category_events] : events) {
|
||||
categories[category] = {
|
||||
{"events", category_events},
|
||||
{"targets", targets[category]},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
{"observed_event_count", m_observations.size()},
|
||||
{"categories", categories},
|
||||
};
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
std::vector<AuditObservation> m_observations;
|
||||
std::vector<ProbeCallbackRecord> m_callbacks;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_PluginProbeCollector_hpp_
|
||||
@@ -0,0 +1,32 @@
|
||||
// Minimal replacement for the config API PythonPluginBridge binds into `orca`.
|
||||
//
|
||||
// The production implementation (PluginConfig.cpp) resolves presets, reads the sidebar's effective
|
||||
// config and writes config.json through PluginManager, all of which pull in the GUI and the
|
||||
// registry. The probe has neither: a capability reports an empty config, an empty version and a
|
||||
// refused save. That keeps the bridge's Python surface intact without pretending to reproduce
|
||||
// application state the probe does not have.
|
||||
|
||||
#include "slic3r/plugin/PluginConfig.hpp"
|
||||
#include "slic3r/plugin/PythonPluginInterface.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
nlohmann::json capability_get_config(const PluginCapabilityInterface&)
|
||||
{
|
||||
return nlohmann::json::object();
|
||||
}
|
||||
|
||||
std::string capability_get_config_version(const PluginCapabilityInterface&) { return {}; }
|
||||
|
||||
bool capability_save_config(const PluginCapabilityInterface&, const nlohmann::json&) { return false; }
|
||||
|
||||
std::string PluginConfig::capability_type_label(PluginCapabilityType type)
|
||||
{
|
||||
return plugin_capability_type_display_name(type);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
@@ -795,20 +794,6 @@ TEST_CASE("Print statistics in custom G-code match the final print statistics",
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Print statistics in custom G-code cannot be used in a calculation", "[Print]")
|
||||
{
|
||||
const std::string key = GENERATE(as<std::string>{}, "total_weight", "used_filament", "total_cost", "print_time", "normal_print_time",
|
||||
"silent_print_time", "total_wipe_tower_cost", "total_wipe_tower_filament", "print_time_total_sec",
|
||||
"print_time_day", "print_time_hour", "print_time_minute", "print_time_sec", "used_filament_length");
|
||||
CAPTURE(key);
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({ { "file_start_gcode", "; {" + key + " + 1}\n" } });
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
CHECK_THROWS_WITH(Slic3r::Test::gcode(print), Catch::Matchers::ContainsSubstring("known only after the G-code is generated"));
|
||||
}
|
||||
|
||||
TEST_CASE("export_gcode writes G-code without a result pointer", "[Print][export_gcode]")
|
||||
{
|
||||
Print print;
|
||||
|
||||
@@ -220,39 +220,11 @@ TEST_CASE("An item larger than the bed cannot be placed", "[Arrange]")
|
||||
ArrangePolygons items;
|
||||
items.emplace_back(make_square(scaled(20.)));
|
||||
items.emplace_back(make_square(scaled(400.))); // far bigger than the bed
|
||||
// An item that cannot be placed keeps the position it came in with, so the
|
||||
// caller can leave it where the user had it.
|
||||
items[1].translation = {scaled(10.), scaled(20.)};
|
||||
|
||||
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
|
||||
|
||||
REQUIRE(items[0].bed_idx == 0);
|
||||
REQUIRE(items[1].bed_idx == UNARRANGED);
|
||||
REQUIRE(items[1].translation.x() == scaled(10.));
|
||||
REQUIRE(items[1].translation.y() == scaled(20.));
|
||||
}
|
||||
|
||||
TEST_CASE("An item filling the printable area is arranged", "[Arrange]")
|
||||
{
|
||||
// The arrange bed keeps a margin from the printable area and the item is widened by
|
||||
// its brim. Neither may keep an item that fits the printable area from being placed.
|
||||
DynamicPrintConfig cfg = bed_config(); // 200 x 200
|
||||
ArrangeParams p = quiet_params();
|
||||
|
||||
ArrangePolygons items = squares(1, 197.);
|
||||
items.front().brim_width = 1.;
|
||||
update_arrange_params(p, &cfg, items);
|
||||
update_selected_items_inflation(items, &cfg, p);
|
||||
arrange(items, get_shrink_bedpts(&cfg, p), p);
|
||||
REQUIRE(items.front().bed_idx == 0);
|
||||
|
||||
// Larger than the printable area is still refused.
|
||||
ArrangeParams p_large = quiet_params();
|
||||
ArrangePolygons large = squares(1, 201.);
|
||||
update_arrange_params(p_large, &cfg, large);
|
||||
update_selected_items_inflation(large, &cfg, p_large);
|
||||
arrange(large, get_shrink_bedpts(&cfg, p_large), p_large);
|
||||
REQUIRE(large.front().bed_idx == UNARRANGED);
|
||||
}
|
||||
|
||||
TEST_CASE("Items overflowing one bed spill onto virtual beds", "[Arrange]")
|
||||
|
||||
@@ -187,23 +187,6 @@ TEST_CASE("A part rests on its largest hull face even when parallel inner faces
|
||||
check_on_bed(plate);
|
||||
}
|
||||
|
||||
TEST_CASE("A box laid on its top face is turned over about X", "[LayOnFace]")
|
||||
{
|
||||
// A face pointing straight up leaves the half turn's axis open to every horizontal
|
||||
// direction. It must be the fixed X flip both times, keep the footprint and not follow
|
||||
// rounding noise to an arbitrary angle.
|
||||
Model model;
|
||||
ModelObject &box = add_box_object(model, { 40, 20, 10 });
|
||||
lay_on_face(box, 0, { 0, 0, 1 });
|
||||
|
||||
check_size(box, { 40, 20, 10 });
|
||||
check_on_bed(box);
|
||||
const Matrix3d rotation = box.instances.front()->get_matrix_no_offset().linear();
|
||||
CHECK_THAT(rotation(0, 0), WithinAbs(1., 1e-9));
|
||||
CHECK_THAT(rotation(1, 1), WithinAbs(-1., 1e-9));
|
||||
CHECK_THAT(rotation(2, 2), WithinAbs(-1., 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("Faces are selected in object coordinates whatever the instance rotation", "[LayOnFace]")
|
||||
{
|
||||
Model model;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_message.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_string.hpp>
|
||||
#include "libslic3r/PlaceholderParser.hpp"
|
||||
@@ -399,53 +398,3 @@ SCENARIO("Placeholder parser names in branches that are not taken", "[Placeholde
|
||||
CHECK(PlaceholderParser::evaluate_boolean_expression("false ? no_such_var == 1 : true", config));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Placeholder parser deferred values can be printed but not computed with", "[PlaceholderParser]") {
|
||||
PlaceholderParser parser;
|
||||
parser.set_deferred("total_weight", "@TOTAL_WEIGHT@");
|
||||
parser.set_deferred("used_filament", "@USED_FILAMENT_LENGTH@");
|
||||
parser.set("label", "W");
|
||||
PlaceholderParser::ContextData context;
|
||||
context.global_config = std::make_unique<DynamicConfig>();
|
||||
auto process = [&parser, &context](const std::string &templ) { return parser.process(templ, 0, nullptr, nullptr, &context); };
|
||||
|
||||
SECTION("printed") {
|
||||
CHECK(process("{total_weight}") == "@TOTAL_WEIGHT@");
|
||||
CHECK(process("[total_weight]") == "@TOTAL_WEIGHT@");
|
||||
CHECK(process("{label + \": \" + total_weight + \"g\"}") == "W: @TOTAL_WEIGHT@g");
|
||||
CHECK(process("{total_weight + \"g, \" + used_filament + \"m\"}") == "@TOTAL_WEIGHT@g, @USED_FILAMENT_LENGTH@m");
|
||||
CHECK(process("{\"W \" + total_weight + \"g for \" + 3 + \" parts\"}") == "W @TOTAL_WEIGHT@g for 3 parts");
|
||||
CHECK(process("{true ? total_weight : 0}") == "@TOTAL_WEIGHT@");
|
||||
CHECK(process("{regex_replace(\"x\", /x/, total_weight)}") == "@TOTAL_WEIGHT@");
|
||||
CHECK(process("{local w = total_weight}{w}") == "@TOTAL_WEIGHT@");
|
||||
}
|
||||
SECTION("used in a calculation or a comparison") {
|
||||
const auto deferred_error = Catch::Matchers::ContainsSubstring("known only after the G-code is generated");
|
||||
for (const std::string templ : {
|
||||
"{total_weight + 1}", "{1 + total_weight}", "{total_weight * 2}", "{-total_weight}", "{int(total_weight)}",
|
||||
"{min(total_weight, 5)}", "{digits(total_weight, 5)}", "{total_weight + used_filament}",
|
||||
"{if total_weight > 100}heavy{endif}", "{if total_weight == \"1\"}one{endif}", "{total_weight and true}",
|
||||
"{total_weight ? 1 : 0}", "{if total_weight =~ /1.*/}one{endif}", "{one_of(total_weight, \"1\")}",
|
||||
"{regex_replace(total_weight, /1/, \"2\")}", "{(\"W\" + total_weight) == \"W1\"}", "{filament_change(total_weight)}",
|
||||
"{regex_replace(\"x\", /x/, total_weight) * 2}", "{if regex_replace(\"x\", /x/, \"W\" + total_weight) > \"W1\"}heavy{endif}",
|
||||
// A deferred value copied to a variable is still deferred.
|
||||
"{local w = total_weight}{w > 1}", "{global g = \"W\" + total_weight}{if g == \"W1\"}one{endif}" }) {
|
||||
CAPTURE(templ);
|
||||
CHECK_THROWS_WITH(process(templ), deferred_error);
|
||||
}
|
||||
// A mistake unrelated to the value being deferred keeps its own message.
|
||||
CHECK_THROWS_WITH(process("{global v = (1, 2)}{global v = \"W\" + total_weight}"),
|
||||
Catch::Matchers::ContainsSubstring("Cannot assign a scalar value to a vector variable"));
|
||||
}
|
||||
SECTION("ignored in a branch that is not taken") {
|
||||
CHECK(process("{if false}{total_weight * 2}{endif}ok") == "ok");
|
||||
struct Restore { ~Restore() { PlaceholderParser::check_inactive_branches = false; } } restore;
|
||||
PlaceholderParser::check_inactive_branches = true;
|
||||
CHECK(process("{if false}{total_weight * 2}{endif}ok") == "ok");
|
||||
}
|
||||
SECTION("other strings keep their behavior") {
|
||||
CHECK(process("{label + 1}") == "W1");
|
||||
CHECK(process("{if label > \"A\"}yes{endif}") == "yes");
|
||||
CHECK(process("{if regex_replace(label, /X/, total_weight) == \"W\"}yes{endif}") == "yes");
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@
|
||||
#include <boost/system/error_code.hpp>
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
#include <libslic3r/libslic3r.h> // GCODEVIEWER_APP_KEY, SLIC3R_APP_KEY (via libslic3r_version.h)
|
||||
#include <slic3r/plugin/PluginAuditManager.hpp>
|
||||
#include <slic3r/plugin/PluginFsUtils.hpp>
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <slic3r/plugin/probe/PluginProbeCollector.hpp>
|
||||
#include <slic3r/plugin/PythonInterpreter.hpp>
|
||||
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
@@ -32,6 +34,7 @@
|
||||
#include <pybind11/pytypes.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <ios>
|
||||
@@ -67,6 +70,26 @@ void seed_denied_keywords()
|
||||
mgr.add_denied_path_keyword(keyword);
|
||||
}
|
||||
|
||||
// Installs the record-only observer for a test and always clears it, so a failed CHECK cannot
|
||||
// leave a callback pointing at a destroyed collector for the next test in this process.
|
||||
struct ScopedTestObservationObserver
|
||||
{
|
||||
explicit ScopedTestObservationObserver(PluginProbeCollector& collector)
|
||||
: m_collector(collector)
|
||||
{
|
||||
PluginAuditManager::instance().set_observation_observer(
|
||||
[this](const AuditObservation& observation) { m_collector.record(observation); });
|
||||
}
|
||||
|
||||
~ScopedTestObservationObserver() { PluginAuditManager::instance().clear_observation_observer(); }
|
||||
|
||||
ScopedTestObservationObserver(const ScopedTestObservationObserver&) = delete;
|
||||
ScopedTestObservationObserver& operator=(const ScopedTestObservationObserver&) = delete;
|
||||
|
||||
private:
|
||||
PluginProbeCollector& m_collector;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Plugin audit denies host-managed filenames anywhere", "[audit]")
|
||||
@@ -667,6 +690,71 @@ const char* const PLUGIN = "AuditPlugin";
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Probe phase scope restores the previous phase", "[audit]")
|
||||
{
|
||||
PluginAuditManager& mgr = PluginAuditManager::instance();
|
||||
mgr.set_current_audit_phase("outer");
|
||||
{
|
||||
ScopedPluginAuditPhase inner("inner");
|
||||
CHECK(mgr.current_audit_phase() == "inner");
|
||||
}
|
||||
CHECK(mgr.current_audit_phase() == "outer");
|
||||
mgr.clear_current_audit_phase();
|
||||
CHECK(mgr.current_audit_phase().empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Record-only probe observations bypass prompts and retain phase attribution", "[audit][Python]")
|
||||
{
|
||||
AuditHarness harness;
|
||||
if (!harness.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
harness.add_plugin(PLUGIN);
|
||||
py::gil_scoped_acquire gil;
|
||||
PythonSide python(harness);
|
||||
|
||||
PluginProbeCollector collector;
|
||||
ScopedTestObservationObserver observer(collector);
|
||||
|
||||
// A declared read permission is recorded and allowed, but must not be written to the sidecar.
|
||||
{
|
||||
ScopedPluginAuditContext context(PLUGIN);
|
||||
CHECK(PluginAuditManager::instance().request_filesystem_read_permissions(PLUGIN, {"/tmp/probe-declared"}));
|
||||
}
|
||||
|
||||
{
|
||||
ScopedPluginAuditPhase phase("probe:callback");
|
||||
python.run_as(PLUGIN, "outcome = attempt(lambda: open(PRIVATE).close())");
|
||||
}
|
||||
|
||||
CHECK(python.str("outcome") == "ok");
|
||||
CHECK(harness.prompts.empty());
|
||||
CHECK(harness.saved_permissions(PLUGIN).fs_read.empty());
|
||||
|
||||
const std::vector<AuditObservation> observations = collector.observations();
|
||||
const auto open_event = std::find_if(
|
||||
observations.begin(), observations.end(), [](const AuditObservation& observation) {
|
||||
return observation.event_name == "open";
|
||||
});
|
||||
REQUIRE(open_event != observations.end());
|
||||
CHECK(open_event->plugin_key == PLUGIN);
|
||||
CHECK(open_event->phase == "probe:callback");
|
||||
CHECK(open_event->category == "FsRead");
|
||||
CHECK(open_event->continued);
|
||||
CHECK(std::find(open_event->targets.begin(), open_event->targets.end(), python.str("PRIVATE")) !=
|
||||
open_event->targets.end());
|
||||
|
||||
const auto declared = std::find_if(observations.begin(), observations.end(), [](const AuditObservation& observation) {
|
||||
return observation.event_name == "orca.request_permissions";
|
||||
});
|
||||
REQUIRE(declared != observations.end());
|
||||
CHECK(declared->category == "FsRead");
|
||||
CHECK(std::find(declared->targets.begin(), declared->targets.end(), "/tmp/probe-declared") != declared->targets.end());
|
||||
|
||||
const nlohmann::json summary = collector.summary();
|
||||
CHECK(summary["categories"].contains("FsRead"));
|
||||
CHECK(summary["observed_event_count"].get<std::size_t>() == observations.size());
|
||||
}
|
||||
|
||||
TEST_CASE("A saved request grant covers the request's own connection in a later session", "[audit][Python]")
|
||||
{
|
||||
AuditHarness harness;
|
||||
@@ -1132,37 +1220,3 @@ TEST_CASE("Stdlib code is recognized through a symlinked Python home", "[audit][
|
||||
CHECK_FALSE(PluginAuditManager::is_stdlib_file((harness.outside.path() / "plugin.py").string()));
|
||||
CHECK_FALSE(PluginAuditManager::is_stdlib_file("<string>"));
|
||||
}
|
||||
|
||||
TEST_CASE("A plugin may read the CPython stdlib without a prompt, but not site-packages or for writing", "[audit][Python]")
|
||||
{
|
||||
AuditHarness harness;
|
||||
if (!harness.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
harness.add_plugin(PLUGIN);
|
||||
py::gil_scoped_acquire gil;
|
||||
PythonSide python(harness);
|
||||
|
||||
const py::dict paths = py::module_::import("sysconfig").attr("get_paths")();
|
||||
const fs::path stdlib = py::str(paths["stdlib"]).cast<std::string>();
|
||||
const fs::path purelib = py::str(paths["purelib"]).cast<std::string>();
|
||||
// os.py ships with every CPython installation. A site-packages path is third-party code even
|
||||
// when the directory is empty: the prompt must come before the missing file is noticed.
|
||||
const fs::path stdlib_file = stdlib / "os.py";
|
||||
REQUIRE(fs::exists(stdlib_file));
|
||||
|
||||
python.globals["STDLIB_FILE"] = stdlib_file.string();
|
||||
python.globals["SITE_PACKAGE_FILE"] = (purelib / "regression_probe.py").string();
|
||||
python.run_as(PLUGIN, R"PY(
|
||||
read_outcome = attempt(lambda: open(STDLIB_FILE).read())
|
||||
write_outcome = attempt(lambda: open(STDLIB_FILE, "r+").close())
|
||||
site_packages_outcome = attempt(lambda: open(SITE_PACKAGE_FILE).close())
|
||||
)PY");
|
||||
|
||||
CHECK(python.str("read_outcome") == "ok");
|
||||
CHECK(python.str("write_outcome") == "denied");
|
||||
CHECK(python.str("site_packages_outcome") == "denied");
|
||||
// Only the write and the site-packages read are asked about; the stdlib read is implicit.
|
||||
const std::string write_prompt = "open: " + python.str("STDLIB_FILE") + "\n";
|
||||
const std::string site_packages_prompt = "open: " + python.str("SITE_PACKAGE_FILE") + "\n";
|
||||
CHECK(describe(harness.prompts) == write_prompt + site_packages_prompt);
|
||||
}
|
||||
Reference in new issue
Block a user