Compare commits

..
Author SHA1 Message Date
Hanif Koh 35e18ef4a4 Run the Flow Rate Fine Pass on the Extruder the Coarse Pass Used
When the fine pass starts from the coarse page the filament comes from
the cached preset, but the extruder was resolved from whatever the
preset page currently has selected. After a restart or reconnect, or if
the selection was changed, that no longer matches and the lookup falls
back to the per-AMS map, which with a Filament Track Switch pins every
AMS to the main extruder. Take the extruder from the cache in that path;
it already records the one the coarse pass was sent with.
2026-10-20 14:15:34 +08:00
Hanif Koh 25922ef344 Show Track-Switch AMS Units Under Both Nozzles in the Calibration Wizard
With a Filament Track Switch installed the device layer pins every AMS to
the main extruder and records the real binding in GetBindedExtruderSet(),
but the calibration preset page still split the AMS units by
GetExtruderId(). Every AMS landed under the right nozzle and the left
nozzle only offered its external spool.

Split the panels by the bound extruder set, blank the external spools when
the switch is installed, and take a selected filament's extruder from the
panel it was picked in rather than from its AMS.
2026-10-20 14:15:34 +08:00
7 changed files with 190 additions and 250 deletions

No files matched your search

+3 -25
View File
@@ -47,7 +47,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
u )
export BUILD_TARGET="universal"
;;
h ) echo "Usage: ./build_release_macos.sh [-d] [-- CMAKE_ARGS...]"
h ) echo "Usage: ./build_release_macos.sh [-d]"
echo " -d: Build deps only"
echo " -a: Set ARCHITECTURE (arm64 or x86_64 or universal)"
echo " -s: Build slicer only"
@@ -60,33 +60,13 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
echo " -- CMAKE_ARGS...: pass everything after '--' to the cmake configure step of the deps (-d) and slicer (-s) builds,"
echo " e.g. -DDESTDIR=<dir> for the deps, -DCMAKE_PREFIX_PATH=<dir>/usr/local -DSLIC3R_PCH=OFF for the slicer, --fresh."
echo " They come last on the cmake command line. Use absolute paths: cmake runs in the build directory."
exit 0
;;
: )
echo "Option -${OPTARG} requires an argument, aborting."
exit 1
;;
* )
echo "Unknown option -${OPTARG}, aborting. CMake arguments must follow '--'."
exit 1
;;
esac
done
# getopts stops at '--'; whatever follows is handed to cmake unchanged.
if [[ ${OPTIND} -le $# ]] ; then
separator_index=$((OPTIND-1))
if [[ "${!separator_index}" != "--" ]] ; then
echo "Unexpected argument '${!OPTIND}': cmake arguments must follow '--', aborting."
exit 1
fi
fi
shift $((OPTIND-1))
EXTRA_CMAKE_ARGS=("$@")
# Set defaults
if [ -z "$ARCH" ]; then
@@ -231,8 +211,7 @@ function build_deps() {
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT} \
"${EXTRA_CMAKE_ARGS[@]}"
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target deps
)
@@ -335,8 +314,7 @@ function build_slicer() {
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT} \
"${EXTRA_CMAKE_ARGS[@]}"
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
)
+15 -7
View File
@@ -435,7 +435,7 @@ void CalibrationWizard::cache_preset_info(MachineObject *obj, float nozzle_dia,
if (obj->is_multi_extruders()) {
int ams_id, slot_id, tray_id;
get_tray_ams_and_slot_id(curr_obj, result.tray_id, ams_id, slot_id, tray_id);
result.extruder_id = preset_page->get_extruder_id(ams_id);
result.extruder_id = preset_page->get_extruder_id_by_tray_id(result.tray_id);
result.nozzle_diameter = preset_page->get_nozzle_diameter(result.extruder_id);
}
else {
@@ -753,7 +753,7 @@ void PressureAdvanceWizard::on_cali_start()
X1CCalibInfos::X1CCalibInfo calib_info;
get_tray_ams_and_slot_id(curr_obj, item.first, calib_info.ams_id, calib_info.slot_id, calib_info.tray_id);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(item.first);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
@@ -792,7 +792,7 @@ void PressureAdvanceWizard::on_cali_start()
CalibInfo calib_info;
calib_info.dev_id = curr_obj->get_dev_id();
get_tray_ams_and_slot_id(curr_obj, selected_filaments.begin()->first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(selected_filaments.begin()->first);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
@@ -877,7 +877,7 @@ void PressureAdvanceWizard::on_cali_start()
calib_info.dev_id = curr_obj->get_dev_id();
get_tray_ams_and_slot_id(curr_obj, item.first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
calib_info.index = preset_page->get_index_by_tray_id(item.first);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(item.first);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
@@ -1252,7 +1252,7 @@ void FlowRateWizard::on_cali_start(CaliPresetStage stage, float cali_value, Flow
X1CCalibInfos::X1CCalibInfo calib_info;
calib_info.tray_id = item.first;
get_tray_ams_and_slot_id(curr_obj, item.first, calib_info.ams_id, calib_info.slot_id, calib_info.tray_id);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(item.first);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
@@ -1308,7 +1308,15 @@ void FlowRateWizard::on_cali_start(CaliPresetStage stage, float cali_value, Flow
int selected_tray_id = 0;
get_tray_ams_and_slot_id(curr_obj, selected_filaments.begin()->first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
calib_info.select_ams = std::to_string(selected_tray_id);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(selected_filaments.begin()->first);
if (from_page == FlowRatioCaliSource::FROM_COARSE_PAGE) {
// The fine pass must run on the extruder the coarse pass used. The cache records it;
// the preset page's selection may have changed or been reset since.
const auto cached = get_cached_selected_filament_for_multi_extruder(curr_obj);
const auto it = cached.find(selected_filaments.begin()->first);
if (it != cached.end())
calib_info.extruder_id = it->second.extruder_id;
}
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
@@ -1724,7 +1732,7 @@ void MaxVolumetricSpeedWizard::on_cali_start()
int selected_tray_id = 0;
get_tray_ams_and_slot_id(curr_obj, selected_filaments.begin()->first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
calib_info.select_ams = std::to_string(selected_tray_id);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(selected_filaments.begin()->first);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
+53 -22
View File
@@ -587,8 +587,23 @@ void CalibrationPresetPage::on_sys_color_changed()
m_ams_sync_button->msw_rescale();
}
int CalibrationPresetPage::get_extruder_id(int ams_id)
int CalibrationPresetPage::get_extruder_id_by_tray_id(int tray_id)
{
// An AMS behind a Filament Track Switch feeds both extruders, so the extruder is the one
// whose panel the filament was selected in.
if (curr_obj && curr_obj->is_multi_extruders()) {
for (FilamentComboBox *fcb : get_selected_filament_combobox()) {
if (fcb->get_tray_id() != tray_id)
continue;
const bool is_deputy = std::find(m_deputy_filament_comboBox_list.begin(), m_deputy_filament_comboBox_list.end(), fcb) !=
m_deputy_filament_comboBox_list.end();
return is_deputy ? DEPUTY_EXTRUDER_ID : MAIN_EXTRUDER_ID;
}
}
int ams_id = tray_id / 4, slot_id, unused_tray_id;
if (curr_obj)
get_tray_ams_and_slot_id(curr_obj, tray_id, ams_id, slot_id, unused_tray_id);
if (m_ams_id_to_extruder_id_map.find(ams_id) != m_ams_id_to_extruder_id_map.end()) {
return m_ams_id_to_extruder_id_map[ams_id];
}
@@ -866,6 +881,18 @@ static void fill_nozzle_volume_combo(ComboBox *combo)
select_nozzle_volume(combo, NozzleVolumeType::nvtStandard);
}
static DynamicPrintConfig empty_tray_config()
{
DynamicPrintConfig config;
config.set_key_value("filament_id", new ConfigOptionStrings{""});
config.set_key_value("tag_uid", new ConfigOptionStrings{""});
config.set_key_value("filament_type", new ConfigOptionStrings{""});
config.set_key_value("tray_name", new ConfigOptionStrings{""});
config.set_key_value("filament_colour", new ConfigOptionStrings{""});
config.set_key_value("filament_exist", new ConfigOptionBools{false});
return config;
}
void CalibrationPresetPage::init_selection_values()
{
// init nozzle diameter and nozzle volume
@@ -2272,27 +2299,36 @@ void CalibrationPresetPage::sync_ams_info(MachineObject* obj)
}
}
// init virtual tray info
if (full_filament_ams_list.find(VIRTUAL_TRAY_MAIN_ID) != full_filament_ams_list.end()) {
filament_ams_list[VIRTUAL_TRAY_MAIN_ID] = full_filament_ams_list[VIRTUAL_TRAY_MAIN_ID];
}
// init virtual tray info. With a Filament Track Switch installed the external spools feed
// through the switch, so they can't be calibrated as their own trays.
if (obj->GetFilaSwitch()->IsInstalled()) {
filament_ams_list[VIRTUAL_TRAY_MAIN_ID] = empty_tray_config();
filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID] = empty_tray_config();
} else {
if (full_filament_ams_list.find(VIRTUAL_TRAY_MAIN_ID) != full_filament_ams_list.end()) {
filament_ams_list[VIRTUAL_TRAY_MAIN_ID] = full_filament_ams_list[VIRTUAL_TRAY_MAIN_ID];
}
if (full_filament_ams_list.find(VIRTUAL_TRAY_DEPUTY_ID) != full_filament_ams_list.end()) {
filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID] = full_filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID];
if (full_filament_ams_list.find(VIRTUAL_TRAY_DEPUTY_ID) != full_filament_ams_list.end()) {
filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID] = full_filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID];
}
}
// update filament from panel, display only obj has ams
// update multi ams panel, display only obj has multi ams
if (obj->HasAms()) {
if (obj->is_multi_extruders()) {
// An AMS behind a Filament Track Switch is bound to both extruders and appears in both panels.
bool main_done = false;
bool deputy_done = false;
for (auto &ams_item : obj->GetFilaSystem()->GetAmsList()) {
if (ams_item.second->GetExtruderId() == 0 && !main_done) {
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), ams_item.second->GetExtruderId());
const auto &extruder_set = ams_item.second->GetBindedExtruderSet();
if (!main_done && extruder_set.count(MAIN_EXTRUDER_ID) != 0) {
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), MAIN_EXTRUDER_ID);
main_done = true;
} else if (ams_item.second->GetExtruderId() == 1 && !deputy_done) {
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), ams_item.second->GetExtruderId());
}
if (!deputy_done && extruder_set.count(DEPUTY_EXTRUDER_ID) != 0) {
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), DEPUTY_EXTRUDER_ID);
deputy_done = true;
}
}
@@ -2333,10 +2369,11 @@ void CalibrationPresetPage::sync_ams_info(MachineObject* obj)
if (ams->second->IsExist()
&& info.parse_ams_info(obj, ams->second, obj->GetFilaSystem()->IsDetectRemainEnabled(), obj->is_support_ams_humidity)) {
ams_info.push_back(info);
if (info.nozzle_id == 0) {
main_ams_info.push_back(info);
} else {
deputy_ams_info.push_back(info);
for (int extruder_id : ams->second->GetBindedExtruderSet()) {
if (extruder_id == MAIN_EXTRUDER_ID)
main_ams_info.push_back(info);
else
deputy_ams_info.push_back(info);
}
}
m_ams_id_to_extruder_id_map[stoi(info.ams_id)] = info.nozzle_id;
@@ -2584,13 +2621,7 @@ void CalibrationPresetPage::update_multi_extruder_filament_combobox(const std::s
}
}
DynamicPrintConfig empty_config;
empty_config.set_key_value("filament_id", new ConfigOptionStrings{""});
empty_config.set_key_value("tag_uid", new ConfigOptionStrings{""});
empty_config.set_key_value("filament_type", new ConfigOptionStrings{""});
empty_config.set_key_value("tray_name", new ConfigOptionStrings{""});
empty_config.set_key_value("filament_colour", new ConfigOptionStrings{""});
empty_config.set_key_value("filament_exist", new ConfigOptionBools{false});
DynamicPrintConfig empty_config = empty_tray_config();
if (filament_ams_list.empty()) return;
@@ -251,7 +251,7 @@ public:
void msw_rescale() override;
void on_sys_color_changed() override;
int get_extruder_id(int ams_id);
int get_extruder_id_by_tray_id(int tray_id);
float get_nozzle_diameter(int extruder_id) const;
NozzleVolumeType get_nozzle_volume_type(int extruder_id) const;
ExtruderType get_extruder_type(int extruder_id) const;
+86 -123
View File
@@ -528,6 +528,81 @@ AuditDecision PluginAuditManager::check_open(const std::string& path_str, const
return check_path_access(boost::filesystem::path(path_str), is_write);
}
bool PluginAuditManager::request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths)
{
if (plugin_key.empty() || paths.empty())
return true;
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
PluginInstallState state;
read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
std::vector<std::string> missing;
for (const std::string& path : paths) {
if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path) == state.permissions.fs_read.end())
missing.push_back(path);
}
if (missing.empty())
return true;
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
auto show_dialog = [&descriptor, &missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format(_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);
}
void PluginAuditManager::report_violation(const AuditViolation& violation)
{
m_last_violation = violation;
@@ -763,7 +838,7 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
// Thread creation exposes no user-supplied target. Use a fixed sentinel so the grant
// persists per plugin: the permission list matches targets by exact string, and the
// started function's repr embeds an address that changes every run.
targets.emplace_back(kThreadPermissionTarget);
targets.emplace_back("thread");
return targets;
default:
break;
@@ -819,20 +894,6 @@ bool has_permission(const std::vector<std::string>& granted, const std::string&
return std::find(granted.begin(), granted.end(), target) != granted.end();
}
// A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata the
// writer needs so clicking Yes still creates the JSON file.
void seed_install_metadata(const PluginDescriptor& descriptor, PluginInstallState& state)
{
if (!state.plugin_name.empty())
return;
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;
}
bool persist_permission(const std::string& plugin_key,
PluginInstallState& state,
std::vector<std::string>& permission_list,
@@ -842,7 +903,15 @@ bool persist_permission(const std::string& plugin_key,
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
seed_install_metadata(descriptor, state);
// 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);
@@ -928,13 +997,6 @@ bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_n
return dialog.ShowModal() == wxID_YES;
}
// Shows a prompt whose body may cover several permission buckets. Must run on the GUI thread.
bool prompt_for_permission_message(const wxString& message)
{
wxMessageDialog dialog(nullptr, message, _L("Plugin permissions"), 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)
@@ -961,7 +1023,7 @@ static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
// 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_permissions runs on -- would therefore deadlock the application (the
// 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
@@ -1085,105 +1147,6 @@ int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, A
} // namespace PluginAuditDetail
bool PluginAuditManager::request_permissions(const std::string& plugin_key, const PluginPermissions& requested)
{
if (plugin_key.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);
// Per bucket, keep only the targets that no earlier grant already covers. Dropping these
// before prompting keeps a repeated declaration from asking again for what is on disk.
struct MissingBucket {
AuditEventCategory category;
std::vector<std::string> targets;
};
std::vector<MissingBucket> missing_buckets;
const auto collect_missing = [&missing_buckets](AuditEventCategory category,
const std::vector<std::string>& requested_targets,
const std::vector<std::string>& granted_targets) {
MissingBucket bucket{category, {}};
for (const std::string& target : requested_targets) {
if (target.empty())
continue;
if (std::find(granted_targets.begin(), granted_targets.end(), target) == granted_targets.end() &&
std::find(bucket.targets.begin(), bucket.targets.end(), target) == bucket.targets.end())
bucket.targets.push_back(target);
}
if (!bucket.targets.empty())
missing_buckets.push_back(std::move(bucket));
};
collect_missing(AuditEventCategory::FsRead, requested.fs_read, state.permissions.fs_read);
collect_missing(AuditEventCategory::FsReadWrite, requested.fs_readwrite, state.permissions.fs_readwrite);
collect_missing(AuditEventCategory::Http, requested.network_http, state.permissions.network_http);
collect_missing(AuditEventCategory::Socket, requested.network_socket, state.permissions.network_socket);
collect_missing(AuditEventCategory::ProcessCreate, requested.process, state.permissions.process);
collect_missing(AuditEventCategory::Threading, requested.threading, state.permissions.threading);
if (missing_buckets.empty())
return true;
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
const auto build_message = [](const std::string& plugin_name, const std::vector<MissingBucket>& buckets) {
wxString message;
for (const MissingBucket& bucket : buckets) {
wxString targets;
for (const std::string& target : bucket.targets)
targets += wxString::FromUTF8(target.c_str()) + "\n";
if (!message.IsEmpty())
message += "\n\n";
message += PluginAuditDetail::audit_message(bucket.category, wxString::FromUTF8(plugin_name.c_str()),
wxEmptyString, targets, bucket.targets.size());
}
return message;
};
bool granted = false;
if (wxIsMainThread()) {
granted = PluginAuditDetail::prompt_for_permission_message(build_message(descriptor.name, missing_buckets));
} else {
auto result = std::make_shared<std::promise<bool>>();
auto future = result->get_future();
GUI::wxGetApp().CallAfter([result, plugin_name = descriptor.name, missing_buckets, build_message]() {
result->set_value(PluginAuditDetail::prompt_for_permission_message(build_message(plugin_name, missing_buckets)));
});
granted = future.get();
}
if (!granted)
return false;
// The dialog can stay open while another grant is persisted; re-read before saving so this
// request adds to the current state instead of replacing it.
PluginInstallState latest_state;
if (read_install_state(boost::filesystem::path(descriptor.plugin_root), latest_state))
state = std::move(latest_state);
PluginAuditDetail::seed_install_metadata(descriptor, state);
for (const MissingBucket& bucket : missing_buckets) {
std::vector<std::string>* permission_list =
PluginAuditDetail::permission_list_for(bucket.category, /*by_event=*/false, state.permissions);
if (!permission_list)
continue;
for (const std::string& target : bucket.targets)
if (std::find(permission_list->begin(), permission_list->end(), target) == permission_list->end())
permission_list->push_back(target);
}
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
}
void PluginAuditManager::record_root_frame()
{
PyObject* frame = reinterpret_cast<PyObject*>(PyEval_GetFrame()); // borrowed reference
+6 -12
View File
@@ -13,12 +13,6 @@
namespace Slic3r {
struct PluginPermissions;
// Thread creation exposes no user-supplied target, so both the runtime audit event and a
// request_permissions(threading=True) declaration use this fixed target.
inline constexpr const char* kThreadPermissionTarget = "thread";
struct AuditDecision {
bool allowed = true;
std::string reason;
@@ -136,12 +130,12 @@ public:
AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write);
AuditDecision check_open(const std::string& path, const std::string& mode);
// Ask the user to grant the requested permissions. The request may originate on a plugin
// load worker, so the implementation marshals the modal dialog to the wx main thread.
// Targets already persisted for the plugin are not shown or asked for again. Returns true
// only when every missing target was granted and persisted; denial aborts the plugin load
// without adding a permission.
bool request_permissions(const std::string& plugin_key, const PluginPermissions& requested);
// Ask the user to grant the requested filesystem-read paths. The request may originate on a
// plugin load worker, so the implementation marshals the modal dialog to the wx main thread.
// Returns true only when every missing path was granted and persisted; denial aborts the plugin
// load without adding a permission.
bool request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths);
void report_violation(const AuditViolation& violation);
bool audit_denial_pending() const;
+26 -60
View File
@@ -49,7 +49,7 @@ thread_local std::string g_active_plugin_key;
std::mutex g_registry_mutex;
std::unordered_map<std::string, std::vector<py::object>> g_pending_capabilities;
std::unordered_map<std::string, py::object> g_pending_package;
std::unordered_map<std::string, PluginPermissions> g_pending_permissions;
std::unordered_map<std::string, std::vector<std::string>> g_pending_fs_read_permissions;
struct PluginInstanceHandle
{
// The C++ plugin interface points into a Python object. Keep both alive through one
@@ -88,7 +88,7 @@ void discard_pending_capture_without_python(const std::string& plugin_key)
g_pending_package.erase(package);
}
g_pending_permissions.erase(plugin_key);
g_pending_fs_read_permissions.erase(plugin_key);
}
} // namespace
@@ -113,7 +113,7 @@ void PythonPluginBridge::begin_plugin_capture(const std::string& plugin_key)
// for this same entry path.
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
g_pending_permissions.erase(plugin_key);
g_pending_fs_read_permissions.erase(plugin_key);
}
// From now until finalize/cancel, @orca.plugin and register_capability() calls made by
// Python code on this thread are attributed to this plugin.
@@ -144,7 +144,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
std::lock_guard<std::mutex> lock(g_registry_mutex);
g_pending_capabilities.erase(capture_key);
g_pending_package.erase(capture_key);
g_pending_permissions.erase(capture_key);
g_pending_fs_read_permissions.erase(capture_key);
};
try {
@@ -177,27 +177,23 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
// Permission declarations are collected while register_capabilities() runs so the
// plugin can describe its needs without touching wx from the Python load worker. Ask
// only after registration returns, before capability instances are materialized.
PluginPermissions requested_permissions;
std::vector<std::string> fs_read_permissions;
{
std::lock_guard<std::mutex> lock(g_registry_mutex);
auto it = g_pending_permissions.find(capture_key);
if (it != g_pending_permissions.end()) {
requested_permissions = std::move(it->second);
g_pending_permissions.erase(it);
auto it = g_pending_fs_read_permissions.find(capture_key);
if (it != g_pending_fs_read_permissions.end()) {
fs_read_permissions = std::move(it->second);
g_pending_fs_read_permissions.erase(it);
}
}
const bool has_permission_request =
!requested_permissions.fs_read.empty() || !requested_permissions.fs_readwrite.empty() ||
!requested_permissions.network_http.empty() || !requested_permissions.network_socket.empty() ||
!requested_permissions.process.empty() || !requested_permissions.threading.empty();
if (has_permission_request) {
if (!fs_read_permissions.empty()) {
bool granted = false;
{
py::gil_scoped_release release;
granted = PluginAuditManager::instance().request_permissions(plugin_key, requested_permissions);
granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions);
}
if (!granted) {
error = _u8L("Plugin permission request denied");
error = _u8L("Plugin filesystem read permission request denied");
BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key;
discard_pending_for_key();
clear_active_key();
@@ -326,7 +322,7 @@ void PythonPluginBridge::cancel_plugin_capture(const std::string& plugin_key)
// may already have registered under this key.
g_pending_capabilities.erase(plugin_key);
g_pending_package.erase(plugin_key);
g_pending_permissions.erase(plugin_key);
g_pending_fs_read_permissions.erase(plugin_key);
}
if (g_active_plugin_key == plugin_key)
@@ -353,7 +349,7 @@ void PythonPluginBridge::clear_pending_captures()
(void) pkg.release();
}
g_pending_package.clear();
g_pending_permissions.clear();
g_pending_fs_read_permissions.clear();
g_active_plugin_key.clear();
return;
}
@@ -362,7 +358,7 @@ void PythonPluginBridge::clear_pending_captures()
BOOST_LOG_TRIVIAL(info) << "Clearing " << g_pending_capabilities.size() << " pending Python plugin capture(s)";
g_pending_capabilities.clear();
g_pending_package.clear();
g_pending_permissions.clear();
g_pending_fs_read_permissions.clear();
g_active_plugin_key.clear();
}
@@ -562,54 +558,24 @@ void bind_python_api(pybind11::module_& m)
m.def(
"request_permissions",
[](const std::vector<std::string>& fs_read,
const std::vector<std::string>& fs_readwrite,
const std::vector<std::string>& network_http,
const std::vector<std::string>& network_socket,
const std::vector<std::string>& process,
bool threading) {
[](const std::vector<std::string>& fs_read) {
if (g_active_plugin_key.empty())
throw py::value_error("request_permissions() called outside plugin discovery context");
std::lock_guard<std::mutex> lock(g_registry_mutex);
PluginPermissions& requested = g_pending_permissions[g_active_plugin_key];
const auto add_targets = [](std::vector<std::string>& bucket, const char* name,
const std::vector<std::string>& targets) {
for (const std::string& target : targets) {
if (target.empty())
throw py::value_error(std::string("request_permissions(") + name +
"=...) does not accept empty values");
if (std::find(bucket.begin(), bucket.end(), target) == bucket.end())
bucket.push_back(target);
}
};
add_targets(requested.fs_read, "fs_read", fs_read);
add_targets(requested.fs_readwrite, "fs_readwrite", fs_readwrite);
add_targets(requested.network_http, "network_http", network_http);
add_targets(requested.network_socket, "network_socket", network_socket);
add_targets(requested.process, "process", process);
if (threading && std::find(requested.threading.begin(), requested.threading.end(),
kThreadPermissionTarget) == requested.threading.end())
requested.threading.emplace_back(kThreadPermissionTarget);
auto& requested = g_pending_fs_read_permissions[g_active_plugin_key];
for (const std::string& path : fs_read) {
if (path.empty())
throw py::value_error("request_permissions(fs_read=...) does not accept empty paths");
if (std::find(requested.begin(), requested.end(), path) == requested.end())
requested.push_back(path);
}
},
py::arg("fs_read") = std::vector<std::string>{},
py::arg("fs_readwrite") = std::vector<std::string>{},
py::arg("network_http") = std::vector<std::string>{},
py::arg("network_socket") = std::vector<std::string>{},
py::arg("process") = std::vector<std::string>{},
py::arg("threading") = false,
R"pbdoc(Request permissions while OrcaSlicer loads your module.
R"pbdoc(Request filesystem read permissions while OrcaSlicer loads your module.
Each bucket takes the target strings the plugin expects the corresponding audit event to
report: filesystem paths for fs_read and fs_readwrite, URLs for network_http, socket addresses
for network_socket, and commands for process. threading=True requests permission to start
threads. Targets already granted to the plugin are not asked for again.
The host presents one request to the user after register_capabilities() returns and before any
capability instance is created. Denying the request aborts the plugin load.)pbdoc");
The host presents the request to the user after register_capabilities() returns. Denying the
request aborts the plugin load.)pbdoc");
m.def("plugin", [](py::object cls) {
if (g_active_plugin_key.empty())