diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 3e8a6478c6..1a1d1bcbff 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8,6 +8,7 @@ #include "I18N.hpp" #include "GCode.hpp" #include "Exception.hpp" +#include "LifecycleEvents.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" #include "Geometry/ConvexHull.hpp" @@ -2476,6 +2477,15 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_writer.set_is_bbl_machine(print->is_BBL_printer()); print->set_started(psGCodeExport); + { + LifecycleEventContext ctx; + ctx.name = std::to_string(print->model().id().id); + ctx.code = LifecycleEvtCode::Ok; + ctx.msg = path; + ctx.cancellation_check = [print]() { return print->canceled(); }; + fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx); + } + // check if any custom gcode contains keywords used by the gcode processor to // produce time estimation and gcode toolpaths std::vector> validation_res = DoExport::validate_custom_gcode(*print); @@ -2511,12 +2521,23 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu m_processor.set_print(print); GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor); if (! file.is_open()) { - BOOST_LOG_TRIVIAL(error) << std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n" << std::endl; + std::string err_msg = std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"; + BOOST_LOG_TRIVIAL(error) << err_msg << std::endl; if (!fs::exists(folder)) { //fs::create_directory(folder); - BOOST_LOG_TRIVIAL(error) << "the parent path " + folder.string() +" is not there!!!" << std::endl; + std::string add_err_msg = "the parent path " + folder.string() +" is not there!!!"; + BOOST_LOG_TRIVIAL(error) << add_err_msg << std::endl; + err_msg += add_err_msg; } - throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"); + { + LifecycleEventContext ctx; + ctx.name = std::to_string(print->model().id().id); + ctx.code = LifecycleEvtCode::Error; + ctx.msg = std::string(path) + "\n" + err_msg; + ctx.cancellation_check = [print]() { return print->canceled(); }; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } + throw Slic3r::RuntimeError(err_msg); } try { @@ -2527,11 +2548,19 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu boost::nowide::remove(path_tmp.c_str()); throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n"); } - } catch (std::exception & /* ex */) { + } catch (std::exception &ex) { // Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown. // Close and remove the file. file.close(); boost::nowide::remove(path_tmp.c_str()); + { + LifecycleEventContext ctx; + ctx.name = std::to_string(print->model().id().id); + ctx.code = LifecycleEvtCode::Error; + ctx.msg = std::string(path) + "\n" + ex.what(); + ctx.cancellation_check = [print]() { return print->canceled(); }; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } throw; } file.close(); @@ -2637,6 +2666,14 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu std::error_code ret = rename_file(path_tmp, path); if (ret) { + { + LifecycleEventContext ctx; + ctx.name = std::to_string(print->model().id().id); + ctx.code = LifecycleEvtCode::Error; + ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message(); + ctx.cancellation_check = [print]() { return print->canceled(); }; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } throw Slic3r::RuntimeError( std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "error code " + ret.message() + '\n' + "Is " + path_tmp + " locked?" + '\n'); @@ -2647,7 +2684,16 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu BOOST_LOG_TRIVIAL(info) << "Exporting G-code finished" << log_memory_info(); print->set_done(psGCodeExport); - + + { + LifecycleEventContext ctx; + ctx.name = std::to_string(print->model().id().id); + ctx.code = LifecycleEvtCode::Ok; + ctx.msg = path; + ctx.cancellation_check = [print]() { return print->canceled(); }; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } + // Orca: label_object_enabled reflects whether objects are labeled in the g-code (EXCLUDE_OBJECT / // M486), which is driven by exclude_object for every printer if(result != nullptr) diff --git a/src/libslic3r/LifecycleEvents.hpp b/src/libslic3r/LifecycleEvents.hpp new file mode 100644 index 0000000000..f3e6f32392 --- /dev/null +++ b/src/libslic3r/LifecycleEvents.hpp @@ -0,0 +1,249 @@ +#pragma once + +// LifecycleEvents.hpp +// -------------------- +// Application lifecycle events (project, slicing, plate editing, preset, printer connection, and +// job activity) that other subsystems -- chiefly the plugin layer above libslic3r -- may want to +// observe. Lives in libslic3r rather than the plugin layer because some events fire from inside +// the slicing engine itself; see fire_lifecycle_event() below. + +#include +#include +#include +#include +#include +#include + +namespace Slic3r +{ + enum class LifecycleEvent { + // Project (3mf) + NewProject, + ProjectOpened, + ProjectBeforeSave, + ProjectAfterSave, + ProjectClosed, + ProjectDirtyChanged, + + // Slicing pipeline + SliceStarted, + SliceGeometryFinished, + GCodeExportStarted, + GCodeExportFinished, + SlicingJobComplete, + + // Plate/model editing + ObjectAdded, + ObjectDeleted, + ObjectTransformed, + ObjectChanged, + ObjectRenamed, + PlateCreated, + PlateDeleted, + PlateSelected, + PlateRenamed, + + // Preset + PresetSelected, + PresetSaved, + + // Printer/device + PrintStateChanged, + DeviceOnline, + DeviceOffline, + DeviceDiscovered, + DeviceSelected, + UploadStarted, + UploadFinished, + + // Print/send jobs + PrintJobStarted, + PrintJobFinished, + SendJobStarted, + SendJobFinished, + }; + + // Scoped so callers must qualify (LifecycleEvtCode::Error, not ERROR) -- ERROR/OK collide with + // Windows macros (wingdi.h) as unqualified names. + enum class LifecycleEvtCode { Ok, Error, Warn }; + + struct LifecycleEventContext + { + // The primary subject identifier for the event. This is event-specific (for example, a + // project/output path, preset name, device id, or object name), must not contain status or + // prose, and may be empty when the event has no single subject. + std::string name; + + // Outcome of the operation represented by the event. For state-change and start events, + // Ok means that the event occurred; it does not imply that a future operation succeeded. + LifecycleEvtCode code = LifecycleEvtCode::Ok; + + // Optional human-readable detail or diagnostic text. It is not a stable parsing contract; + // machine-readable data should be represented by a dedicated field or event instead. + std::string msg; + + // Stable subject/object identifier, when the source model provides one. + std::string id; + + // Previous value for rename and other before/after events. + std::string previous_name; + + // Device identifier for printer and job events. + std::string device_id; + + // Job identifier when the originating queue/task provides one. + std::string job_id; + + // Source subsystem or operation detail, suitable for filtering but not guaranteed to be + // exhaustive across versions. + std::string source; + + // Plate, object, or volume index when the source uses an index rather than a stable id. + int index = -1; + + // Aggregate project dirty state for ProjectDirtyChanged. + bool dirty = false; + + // Optional host-side cancellation probe. Background slicing and G-code export events set + // this to the originating Print's cancellation state so dispatch can stop before calling + // the next capability. It is intentionally not exposed through the Python payload API. + std::function cancellation_check; + }; + + inline std::string lifecycle_event_to_string(LifecycleEvent event) + { + switch (event) { + case LifecycleEvent::NewProject: return "NewProject"; + case LifecycleEvent::ProjectOpened: return "ProjectOpened"; + case LifecycleEvent::ProjectBeforeSave: return "ProjectBeforeSave"; + case LifecycleEvent::ProjectAfterSave: return "ProjectAfterSave"; + case LifecycleEvent::ProjectClosed: return "ProjectClosed"; + case LifecycleEvent::ProjectDirtyChanged: return "ProjectDirtyChanged"; + + case LifecycleEvent::SliceStarted: return "SliceStarted"; + case LifecycleEvent::SliceGeometryFinished: return "SliceGeometryFinished"; + case LifecycleEvent::GCodeExportStarted: return "GCodeExportStarted"; + case LifecycleEvent::GCodeExportFinished: return "GCodeExportFinished"; + case LifecycleEvent::SlicingJobComplete: return "SlicingJobComplete"; + + case LifecycleEvent::ObjectAdded: return "ObjectAdded"; + case LifecycleEvent::ObjectDeleted: return "ObjectDeleted"; + case LifecycleEvent::ObjectTransformed: return "ObjectTransformed"; + case LifecycleEvent::ObjectChanged: return "ObjectChanged"; + case LifecycleEvent::ObjectRenamed: return "ObjectRenamed"; + case LifecycleEvent::PlateCreated: return "PlateCreated"; + case LifecycleEvent::PlateDeleted: return "PlateDeleted"; + case LifecycleEvent::PlateSelected: return "PlateSelected"; + case LifecycleEvent::PlateRenamed: return "PlateRenamed"; + + case LifecycleEvent::PresetSelected: return "PresetSelected"; + case LifecycleEvent::PresetSaved: return "PresetSaved"; + case LifecycleEvent::PrintStateChanged: return "PrintStateChanged"; + + case LifecycleEvent::DeviceOnline: return "DeviceOnline"; + case LifecycleEvent::DeviceOffline: return "DeviceOffline"; + case LifecycleEvent::DeviceDiscovered: return "DeviceDiscovered"; + case LifecycleEvent::DeviceSelected: return "DeviceSelected"; + + case LifecycleEvent::UploadStarted: return "UploadStarted"; + case LifecycleEvent::UploadFinished: return "UploadFinished"; + case LifecycleEvent::PrintJobStarted: return "PrintJobStarted"; + case LifecycleEvent::PrintJobFinished: return "PrintJobFinished"; + case LifecycleEvent::SendJobStarted: return "SendJobStarted"; + case LifecycleEvent::SendJobFinished: return "SendJobFinished"; + default: return "Unknown"; + } + } + + inline std::string lifecycle_evt_code_to_string(LifecycleEvtCode code) + { + switch (code) { + case LifecycleEvtCode::Ok: return "Ok"; + case LifecycleEvtCode::Error: return "Error"; + case LifecycleEvtCode::Warn: return "Warn"; + default: return "Unknown"; + } + } + + // Global cross-layer seam (mirrors ConfigBase::set_resolve_capability_fn): any libslic3r code can + // fire a lifecycle event without depending on the plugin layer above it, which installs the + // dispatcher here at startup. Not tied to Print/GCode specifically, since nothing here should + // require callers to hold a Print& just to report an event. + using LifecycleHookFn = std::function; + + namespace detail { + + struct LifecycleHookState + { + std::mutex mutex; + std::condition_variable cv; + LifecycleHookFn fn; + std::size_t active_dispatches = 0; + bool accepting = false; + }; + + inline LifecycleHookState& lifecycle_hook_state() + { + static LifecycleHookState state; + return state; + } + + class LifecycleDispatchGuard + { + public: + explicit LifecycleDispatchGuard(LifecycleHookState& state) : m_state(state) {} + + ~LifecycleDispatchGuard() + { + std::lock_guard lock(m_state.mutex); + --m_state.active_dispatches; + if (m_state.active_dispatches == 0) + m_state.cv.notify_all(); + } + + LifecycleDispatchGuard(const LifecycleDispatchGuard&) = delete; + LifecycleDispatchGuard& operator=(const LifecycleDispatchGuard&) = delete; + + private: + LifecycleHookState& m_state; + }; + + } // namespace detail + + // Installing a hook starts accepting dispatches. Passing an empty function stops accepting + // new dispatches, detaches the hook, and waits for callbacks already in progress to finish. + // This is used during plugin shutdown so plugin code cannot be unloaded while a lifecycle + // callback is still executing. The empty-function path must not be called from inside the + // lifecycle callback itself. + inline void set_lifecycle_hook_fn(LifecycleHookFn fn) + { + detail::LifecycleHookState& state = detail::lifecycle_hook_state(); + if (fn) { + std::lock_guard lock(state.mutex); + state.fn = std::move(fn); + state.accepting = true; + return; + } + + std::unique_lock lock(state.mutex); + state.accepting = false; + state.fn = nullptr; + state.cv.wait(lock, [&state] { return state.active_dispatches == 0; }); + } + + inline void fire_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) + { + detail::LifecycleHookState& state = detail::lifecycle_hook_state(); + LifecycleHookFn fn; + { + std::lock_guard lock(state.mutex); + if (!state.accepting || !state.fn) + return; + fn = state.fn; + ++state.active_dispatches; + } + + detail::LifecycleDispatchGuard guard(state); + fn(event, ctx); + } +} diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 1002f5be89..214ef229df 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -47,6 +47,7 @@ #include #include "libslic3r.h" +#include "LifecycleEvents.hpp" #include "Utils.hpp" #include "Time.hpp" #include "PlaceholderParser.hpp" @@ -2972,6 +2973,7 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det // 1) Find the preset with a new_name or create a new one, // initialize it with the edited config. auto it = this->find_preset_internal(new_name); + const bool preset_existed = (it != m_presets.end() && it->name == new_name); if (it != m_presets.end() && it->name == new_name) { // Preset with the same name found. Preset &preset = *it; @@ -3079,6 +3081,14 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det this->get_selected_preset().save(&(parent_preset->config)); else this->get_selected_preset().save(nullptr); + + { + LifecycleEventContext ctx; + ctx.name = new_name; + ctx.msg = preset_existed ? "overwrite" : "new"; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::PresetSaved, ctx); + } } // A detached standalone preset for the Full Publish receiver: create a user preset holding diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 833b811535..5df69b7335 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -14,6 +14,7 @@ #include "Flow.hpp" #include "Geometry/ConvexHull.hpp" #include "I18N.hpp" +#include "LifecycleEvents.hpp" #include "ShortestPath.hpp" #include "Thread.hpp" #include "Time.hpp" @@ -2694,6 +2695,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) if (m_objects.empty()) return; + { + LifecycleEventContext ctx; + ctx.name = std::to_string(m_model.id().id); + ctx.code = LifecycleEvtCode::Ok; + ctx.cancellation_check = [this]() { return canceled(); }; + fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx); + } + for (PrintObject *obj : m_objects) obj->clear_shared_object(); @@ -3312,6 +3321,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) } BOOST_LOG_TRIVIAL(info) << "Slicing process finished." << log_memory_info(); + + { + LifecycleEventContext ctx; + ctx.name = std::to_string(m_model.id().id); + ctx.code = LifecycleEvtCode::Ok; + ctx.cancellation_check = [this]() { return canceled(); }; + fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx); + } } // G-code export process, running at a background thread. @@ -4911,6 +4928,15 @@ void Print::set_gcode_file_invalidated() //BBS: add gcode file preload logic void Print::export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb) { + { + LifecycleEventContext ctx; + ctx.name = std::to_string(m_model.id().id); + ctx.code = LifecycleEvtCode::Ok; + ctx.msg = file; + ctx.cancellation_check = [this]() { return canceled(); }; + fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx); + } + try { GCodeProcessor processor; GCodeProcessor::s_IsBBLPrinter = is_BBL_printer(); @@ -4930,13 +4956,30 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces *result = std::move(processor.extract_result()); result->filament_change_sequence = filament_seq_loaded; result->nozzle_change_sequence = nozzle_seq_loaded; - } catch (std::exception & /* ex */) { + } catch (std::exception &ex) { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str(); + { + LifecycleEventContext ctx; + ctx.name = std::to_string(m_model.id().id); + ctx.code = LifecycleEvtCode::Error; + ctx.msg = file + "\n" + ex.what(); + ctx.cancellation_check = [this]() { return canceled(); }; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } throw Slic3r::RuntimeError( std::string("Failed to process the G-code file ") + file + " from previous 3mf\n"); } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": process the G-code file %1% successfully")%file.c_str(); + + { + LifecycleEventContext ctx; + ctx.name = std::to_string(m_model.id().id); + ctx.code = LifecycleEvtCode::Ok; + ctx.msg = file; + ctx.cancellation_check = [this]() { return canceled(); }; + fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx); + } } std::tuple Print::object_skirt_offset(double margin_height) const diff --git a/src/slic3r/GUI/DeviceCore/DevManager.cpp b/src/slic3r/GUI/DeviceCore/DevManager.cpp index feb6301df3..c03f0cfe59 100644 --- a/src/slic3r/GUI/DeviceCore/DevManager.cpp +++ b/src/slic3r/GUI/DeviceCore/DevManager.cpp @@ -10,6 +10,7 @@ #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "slic3r/Utils/NetworkAgentFactory.hpp" #include "libslic3r/Time.hpp" @@ -355,7 +356,10 @@ namespace Slic3r obj->bind_state = "free"; obj->last_alive = Slic3r::Utils::get_current_time_utc(); - obj->m_is_online = true; + // Route through set_online_state() (rather than writing m_is_online directly) so the + // DeviceOnline lifecycle event fires consistently; same effective value/behavior + // here since the object was already online in the common case. + obj->set_online_state(true); obj->set_dev_name(dev_name); /* if (!obj->dev_ip.empty()) { Slic3r::GUI::wxGetApp().app_config->set_str("ip_address", obj->dev_id, obj->dev_ip); @@ -373,6 +377,10 @@ namespace Slic3r obj->bind_sec_link = sec_link; obj->dev_connection_name = connection_name; obj->bind_ssdp_version = ssdp_version; + // Discovery establishes the initial reachability state. Do not report it as an + // online transition; DeviceDiscovered below is the lifecycle event for a new + // device. Subsequent updates route through set_online_state(), so a known device + // still emits DeviceOnline/DeviceOffline when its reachability actually changes. obj->m_is_online = true; //load access code @@ -389,6 +397,15 @@ namespace Slic3r BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " New Machine, dev_id= " << dev_id << ", ip = " << dev_ip <<", printer_name = " << dev_name << ", con_type= " << connect_type <<", signal= " << printer_signal << ", bind_state= " << bind_state; + + // First discovery of a genuinely new device (not a periodic SSDP/heartbeat update to + // an already-known one, which is handled in the branch above). + { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceDiscovered, ctx); + } } update_local_machine(*obj); } @@ -984,8 +1001,14 @@ namespace Slic3r } void DeviceManager::OnSelectedMachineChanged(const std::string& /*pre_dev_id*/, - const std::string& /*new_dev_id*/) + const std::string& new_dev_id) { + { + LifecycleEventContext ctx; + ctx.name = new_dev_id; // empty string is a valid deselection + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::DeviceSelected, ctx); + } if (MachineObject* obj_ = get_selected_machine()) { GUI::wxGetApp().sidebar().update_sync_status(obj_); if(m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription) diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index fd60f80d25..6bd690e9a6 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -5,6 +5,7 @@ #include "libslic3r/Time.hpp" #include "libslic3r/Thread.hpp" #include "slic3r/Utils/NetworkAgent.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "slic3r/Utils/NetworkAgentFactory.hpp" #include "GuiColor.hpp" @@ -2626,7 +2627,15 @@ void MachineObject::reset() void MachineObject::set_print_state(std::string status) { + const bool changed = (print_status != status); print_status = status; + if (changed) { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + ctx.msg = print_status; + fire_lifecycle_event(LifecycleEvent::PrintStateChanged, ctx); + } } int MachineObject::connect(bool use_openssl) @@ -2648,7 +2657,10 @@ int MachineObject::connect(bool use_openssl) int MachineObject::disconnect() { if (m_agent) { - return m_agent->disconnect_printer(); + const int result = m_agent->disconnect_printer(); + if (result == 0) + set_online_state(false); + return result; } return -1; } @@ -2678,8 +2690,16 @@ bool MachineObject::is_connecting() void MachineObject::set_online_state(bool on_off) { + const bool changed = (m_is_online != on_off); m_is_online = on_off; if (!on_off) m_active_state = NotActive; + if (changed) { + LifecycleEventContext ctx; + ctx.name = dev_id; + ctx.code = LifecycleEvtCode::Ok; + ctx.msg = on_off ? "online" : "offline"; + fire_lifecycle_event(on_off ? LifecycleEvent::DeviceOnline : LifecycleEvent::DeviceOffline, ctx); + } } bool MachineObject::is_info_ready(bool check_version) const @@ -2806,13 +2826,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ parse_msg_count++; std::chrono::system_clock::time_point clock_start = std::chrono::system_clock::now(); - this->set_online_state(true); - - std::chrono::system_clock::time_point curr_time = std::chrono::system_clock::now(); - auto diff1 = std::chrono::duration_cast(curr_time - last_update_time); - - /* update last received time */ - last_update_time = std::chrono::system_clock::now(); json j_pre; bool parse_ok = false; @@ -2825,8 +2838,29 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ /* post process payload */ sanitizeToUtf8(payload); BOOST_LOG_TRIVIAL(info) << "parse_json: sanitize to utf8"; + try { + j_pre = json::parse(payload); + parse_ok = true; + } + catch (...) {} } + bool client_disconnected = false; + if (parse_ok && j_pre.is_object() && j_pre.contains("event") && j_pre["event"].is_object() && + j_pre["event"].contains("event") && j_pre["event"]["event"].is_string()) { + client_disconnected = j_pre["event"]["event"].get() == "client.disconnected"; + } + + // A disconnect notification is a transport message too, but it must not first mark an + // already-offline device as online through the generic message-received path. + set_online_state(!client_disconnected); + + std::chrono::system_clock::time_point curr_time = std::chrono::system_clock::now(); + auto diff1 = std::chrono::duration_cast(curr_time - last_update_time); + + /* update last received time */ + last_update_time = std::chrono::system_clock::now(); + try { bool restored_json = false; json j; @@ -4597,19 +4631,6 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_ } } - // event info - try { - if (j.contains("event")) { - if (j["event"].contains("event")) { - if (j["event"]["event"].get() == "client.disconnected") - set_online_state(false); - else if (j["event"]["event"].get() == "client.connected") - set_online_state(true); - } - } - } - catch (...) {} - if (!key_field_only) { BOOST_LOG_TRIVIAL(trace) << "parse_json m_active_state =" << m_active_state; parse_state_changed_event(); diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 914c8f7868..ca3b7c3c44 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -897,6 +897,7 @@ public: bool is_connected(); bool is_connecting(); + // Emits DeviceOnline or DeviceOffline only when the reachability state changes. void set_online_state(bool on_off); bool is_online() { return m_is_online; } bool is_info_ready(bool check_version = true) const; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index b76c7f346c..887d13aedc 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -43,6 +43,7 @@ #include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp" #include "slic3r/Utils/UndoRedo.hpp" #include "slic3r/Utils/MacDarkMode.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include @@ -5028,8 +5029,16 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) //BBS: nofity object list to update wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items(); - if (object_moved) + if (object_moved) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "moved"; + if (done.size() == 1) + ctx.name = m_model->objects[done.begin()->first]->name; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_MOVED)); + } // BBS: support wipe-tower for multi-plates for (int plate_id = 0; plate_id < wipe_tower_origins.size(); plate_id++) { @@ -5150,8 +5159,16 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) //BBS: nofity object list to update wxGetApp().plater()->sidebar().obj_list()->update_plate_values_for_items(); - if (!done.empty()) + if (!done.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "rotated"; + if (done.size() == 1) + ctx.name = m_model->objects[done.begin()->first]->name; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED)); + } m_dirty = true; } @@ -5242,8 +5259,16 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) //BBS: notify object info update wxGetApp().plater()->show_object_info(); - if (!done.empty()) + if (!done.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "scaled"; + if (done.size() == 1) + ctx.name = m_model->objects[done.begin()->first]->name; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED)); + } m_dirty = true; } diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 48bd58b1a5..29bab18aeb 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -2182,6 +2182,8 @@ void GUI_App::init_networking_callbacks() obj->command_get_access_code(); if (m_agent) m_agent->install_device_cert(obj->get_dev_id(), obj->is_lan_mode_printer()); + + obj->set_online_state(true); } }); }); @@ -2220,6 +2222,8 @@ void GUI_App::init_networking_callbacks() obj->command_get_version(); event.SetInt(0); event.SetString(obj->get_dev_id()); + + obj->set_online_state(true); } else if (state == ConnectStatus::ConnectStatusFailed) { // Orca: only update status if same device id if (m_device_manager->selected_machine != dev_id) return; @@ -2235,10 +2239,14 @@ void GUI_App::init_networking_callbacks() wxGetApp().show_dialog(text); } event.SetInt(-1); + + obj->set_online_state(false); } else if (state == ConnectStatus::ConnectStatusLost) { m_device_manager->set_selected_machine(""); event.SetInt(-1); BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = lost"; + + obj->set_online_state(false); } else { event.SetInt(-1); BOOST_LOG_TRIVIAL(info) << "set_on_local_connect_fn: state = " << state; diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 2d77200eef..5220e026c7 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1,5 +1,6 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/PresetBundle.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "GUI_ObjectList.hpp" #include "GUI_Factories.hpp" //#include "GUI_ObjectLayers.hpp" @@ -10,6 +11,7 @@ #include "BitmapComboBox.hpp" #include "MainFrame.hpp" #include "slic3r/Utils/UndoRedo.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "OptionsGroup.hpp" #include "Tab.hpp" @@ -1159,17 +1161,39 @@ void ObjectList::update_name_in_model(const wxDataViewItem& item) const if (m_objects_model->GetItemType(item) & itObject) { std::string name = m_objects_model->GetName(item).ToUTF8().data(); if (obj->name != name) { + const std::string previous_name = obj->name; obj->name = name; // if object has just one volume, rename this volume too if (obj->volumes.size() == 1) obj->volumes[0]->name = obj->name; Slic3r::save_object_mesh(*obj); + + LifecycleEventContext ctx; + ctx.name = name; + ctx.previous_name = previous_name; + ctx.id = std::to_string(obj->id().id); + ctx.index = obj_idx; + ctx.source = "object"; + fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx); } return; } if (volume_id < 0) return; - obj->volumes[volume_id]->name = m_objects_model->GetName(item).ToUTF8().data(); + std::string name = m_objects_model->GetName(item).ToUTF8().data(); + if (obj->volumes[volume_id]->name == name) + return; + + const std::string previous_name = obj->volumes[volume_id]->name; + obj->volumes[volume_id]->name = name; + + LifecycleEventContext ctx; + ctx.name = name; + ctx.previous_name = previous_name; + ctx.id = std::to_string(obj->volumes[volume_id]->id().id); + ctx.index = volume_id; + ctx.source = "volume"; + fire_lifecycle_event(LifecycleEvent::ObjectRenamed, ctx); } void ObjectList::update_name_in_list(int obj_idx, int vol_idx) const @@ -3519,7 +3543,14 @@ void ObjectList::delete_all_connectors_for_object(int obj_idx) obj->delete_connectors(); if (obj->volumes.empty() || !obj->has_solid_mesh()) { + const std::string deleted_obj_name = obj->name; model.delete_object(idx); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = deleted_obj_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx); + } m_objects_model->Delete(m_objects_model->GetItemById(idx)); continue; } @@ -4088,6 +4119,13 @@ void ObjectList::add_object_to_list(size_t obj_idx, bool call_selection_changed, const auto item = m_objects_model->AddObject(model_object, warning_bitmap, model_object->is_cut()); Expand(m_objects_model->GetParent(item)); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = model_object->name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectAdded, ctx); + } + if (!do_info_update) return; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index c0cc7bdb5a..438bda2cc2 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -13,6 +13,7 @@ #include "slic3r/GUI/NotificationManager.hpp" #include "slic3r/GUI/format.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "libnest2d/common.hpp" @@ -698,6 +699,13 @@ void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) { BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":arrange m_unprintable: name: %4%, bed_id %1%, trans {%2%,%3%}") % ap.bed_idx % unscale(ap.translation(X)) % unscale(ap.translation(Y)) % ap.name; } + { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "arranged"; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + } + m_plater->update(); // BBS //wxGetApp().obj_manipul()->set_dirty(); diff --git a/src/slic3r/GUI/Jobs/FillBedJob.cpp b/src/slic3r/GUI/Jobs/FillBedJob.cpp index 5364cb5648..961b8af566 100644 --- a/src/slic3r/GUI/Jobs/FillBedJob.cpp +++ b/src/slic3r/GUI/Jobs/FillBedJob.cpp @@ -6,6 +6,7 @@ #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "libnest2d/common.hpp" #include @@ -347,6 +348,13 @@ void FillBedJob::finalize(bool canceled, std::exception_ptr &eptr) m_plater->arrange(); } m_plater->update(); + + { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "arranged"; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + } } m_plater->mark_plate_toolbar_image_dirty(); diff --git a/src/slic3r/GUI/Jobs/OrientJob.cpp b/src/slic3r/GUI/Jobs/OrientJob.cpp index ee8ea875c0..ed9117e8bf 100644 --- a/src/slic3r/GUI/Jobs/OrientJob.cpp +++ b/src/slic3r/GUI/Jobs/OrientJob.cpp @@ -5,6 +5,7 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/NotificationManager.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "libslic3r/PresetBundle.hpp" @@ -254,6 +255,12 @@ void OrientJob::finalize(bool canceled, std::exception_ptr &eptr) mesh.apply(); } + if (!m_selected.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + ctx.msg = "auto_oriented"; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectTransformed, ctx); + } m_plater->update(); diff --git a/src/slic3r/GUI/Jobs/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp index dae05494e3..76e45eb1d7 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.cpp +++ b/src/slic3r/GUI/Jobs/PrintJob.cpp @@ -1,4 +1,5 @@ #include "PrintJob.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "libslic3r/MTUtils.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/PresetBundle.hpp" @@ -152,6 +153,18 @@ void PrintJob::process(Ctl &ctl) int result = -1; std::string http_body; + const auto mark_lifecycle_started = [this]() { + if (m_lifecycle_started) + return; + + LifecycleEventContext start_ctx; + start_ctx.name = m_project_name; + start_ctx.device_id = m_dev_id; + start_ctx.source = "print_job"; + fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx); + m_lifecycle_started = true; + }; + int total_plate_num = plate_data.plate_count; if (!plate_data.is_valid) { total_plate_num = m_plater->get_partplate_list().get_plate_count(); @@ -173,7 +186,7 @@ void PrintJob::process(Ctl &ctl) } } - m_project_name = truncate_string(m_project_name, 100); + const std::string transport_project_name = truncate_string(m_project_name, 100); int curr_plate_idx = 0; if (m_print_type == "from_normal") { @@ -372,11 +385,11 @@ void PrintJob::process(Ctl &ctl) } } - if (params.preset_name.empty() && m_print_type == "from_normal") { params.preset_name = wxString::Format("%s_plate_%d", m_project_name, curr_plate_idx).ToStdString(); } - if (params.project_name.empty()) {params.project_name = m_project_name;} + if (params.preset_name.empty() && m_print_type == "from_normal") { params.preset_name = wxString::Format("%s_plate_%d", transport_project_name, curr_plate_idx).ToStdString(); } + if (params.project_name.empty()) {params.project_name = transport_project_name;} if (m_is_calibration_task) { - params.project_name = m_project_name; + params.project_name = transport_project_name; params.origin_model_id = ""; } @@ -543,6 +556,7 @@ void PrintJob::process(Ctl &ctl) if (m_print_type == "from_sdcard_view") { BOOST_LOG_TRIVIAL(info) << "print_job: try to send with cloud, model is sdcard view"; ctl.update_status(curr_percent, _u8L("Sending print job through cloud service")); + mark_lifecycle_started(); result = m_agent->start_sdcard_print(params, update_fn, cancel_fn); } else if (params.connection_type != "lan") { if (params.dev_ip.empty()) @@ -566,6 +580,7 @@ void PrintJob::process(Ctl &ctl) BOOST_LOG_TRIVIAL(info) << "print_job: use ftp send print only"; ctl.update_status(curr_percent, _u8L("Sending print job over LAN")); is_try_lan_mode = true; + mark_lifecycle_started(); result = m_agent->start_local_print_with_record(params, update_fn, cancel_fn, wait_fn); if (result < 0) { error_text = wxString::Format(_L("Access code:%s IP address:%s"), params.password, params.dev_ip); @@ -582,6 +597,7 @@ void PrintJob::process(Ctl &ctl) // try to send local with record BOOST_LOG_TRIVIAL(info) << "print_job: try to start local print with record"; ctl.update_status(curr_percent, _u8L("Sending print job over LAN")); + mark_lifecycle_started(); result = m_agent->start_local_print_with_record(params, update_fn, cancel_fn, wait_fn); if (result == 0) { params.comments = ""; @@ -597,18 +613,21 @@ void PrintJob::process(Ctl &ctl) // try to send with cloud BOOST_LOG_TRIVIAL(warning) << "print_job: try to send with cloud"; ctl.update_status(curr_percent, _u8L("Sending print job through cloud service")); + // Started was already emitted before the local attempt. result = m_agent->start_print(params, update_fn, cancel_fn, wait_fn); } } else { BOOST_LOG_TRIVIAL(info) << "print_job: send with cloud"; ctl.update_status(curr_percent, _u8L("Sending print job through cloud service")); + mark_lifecycle_started(); result = m_agent->start_print(params, update_fn, cancel_fn, wait_fn); } } } else { if (this->could_emmc_print) { ctl.update_status(curr_percent, _u8L("Sending print job over LAN")); + mark_lifecycle_started(); result = m_agent->start_local_print(params, update_fn, cancel_fn); } else { switch(this->sdcard_state) { @@ -619,6 +638,7 @@ void PrintJob::process(Ctl &ctl) if(this->has_sdcard) { // means the storage is abnormal but can be used option is enabled ctl.update_status(curr_percent, _u8L("Sending print job over LAN, but the Storage in the printer is abnormal and print-issues may be caused by this.")); + mark_lifecycle_started(); result = m_agent->start_local_print(params, update_fn, cancel_fn); break; } @@ -629,6 +649,7 @@ void PrintJob::process(Ctl &ctl) return; case DevStorage::SdcardState::HAS_SDCARD_NORMAL: ctl.update_status(curr_percent, _u8L("Sending print job over LAN")); + mark_lifecycle_started(); result = m_agent->start_local_print(params, update_fn, cancel_fn); break; default: @@ -686,6 +707,7 @@ void PrintJob::process(Ctl &ctl) } wxQueueEvent(m_plater, evt); m_job_finished = true; + m_lifecycle_success = true; } } @@ -698,6 +720,19 @@ void PrintJob::finalize(bool canceled, std::exception_ptr &eptr) { eptr = std::current_exception(); } + if (m_lifecycle_started && !m_lifecycle_finished) { + LifecycleEventContext finish_ctx; + finish_ctx.name = m_project_name; + finish_ctx.device_id = m_dev_id; + finish_ctx.source = "print_job"; + finish_ctx.code = canceled ? LifecycleEvtCode::Warn : + (eptr || !m_lifecycle_success ? LifecycleEvtCode::Error : LifecycleEvtCode::Ok); + finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" : + (m_lifecycle_success ? "" : "failed")); + fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx); + m_lifecycle_finished = true; + } + if (canceled || eptr) return; } diff --git a/src/slic3r/GUI/Jobs/PrintJob.hpp b/src/slic3r/GUI/Jobs/PrintJob.hpp index 9a23b04054..4be68031d9 100644 --- a/src/slic3r/GUI/Jobs/PrintJob.hpp +++ b/src/slic3r/GUI/Jobs/PrintJob.hpp @@ -43,6 +43,9 @@ class PrintJob : public Job std::function m_success_fun{nullptr}; std::string m_dev_id; bool m_job_finished{ false }; + bool m_lifecycle_started{ false }; + bool m_lifecycle_finished{ false }; + bool m_lifecycle_success{ false }; int m_print_job_completed_id = 0; wxString m_completed_evt_data; std::function m_enter_ip_address_fun_fail{ nullptr }; diff --git a/src/slic3r/GUI/Jobs/SendJob.cpp b/src/slic3r/GUI/Jobs/SendJob.cpp index d27b18f24b..505475e4a9 100644 --- a/src/slic3r/GUI/Jobs/SendJob.cpp +++ b/src/slic3r/GUI/Jobs/SendJob.cpp @@ -1,4 +1,5 @@ #include "SendJob.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/Utils/NetworkAgent.hpp" #include "libslic3r/MTUtils.hpp" @@ -148,6 +149,13 @@ void SendJob::process(Ctl &ctl) } } + LifecycleEventContext start_ctx; + start_ctx.name = m_project_name; + start_ctx.device_id = m_dev_id; + start_ctx.source = "send_job"; + fire_lifecycle_event(LifecycleEvent::SendJobStarted, start_ctx); + m_lifecycle_started = true; + int total_plate_num = m_plater->get_partplate_list().get_plate_count(); PartPlate* plate = m_plater->get_partplate_list().get_plate(job_data.plate_idx); @@ -424,6 +432,19 @@ void SendJob::finalize(bool canceled, std::exception_ptr &eptr) eptr = std::current_exception(); } + if (m_lifecycle_started && !m_lifecycle_finished) { + LifecycleEventContext finish_ctx; + finish_ctx.name = m_project_name; + finish_ctx.device_id = m_dev_id; + finish_ctx.source = "send_job"; + finish_ctx.code = canceled ? LifecycleEvtCode::Warn : + (eptr ? LifecycleEvtCode::Error : (m_job_finished ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error)); + finish_ctx.msg = canceled ? "cancelled" : (eptr ? "exception" : + (m_job_finished ? "" : "failed")); + fire_lifecycle_event(LifecycleEvent::SendJobFinished, finish_ctx); + m_lifecycle_finished = true; + } + if (canceled || eptr) return; } diff --git a/src/slic3r/GUI/Jobs/SendJob.hpp b/src/slic3r/GUI/Jobs/SendJob.hpp index 1d4e025d6e..86d180ac8c 100644 --- a/src/slic3r/GUI/Jobs/SendJob.hpp +++ b/src/slic3r/GUI/Jobs/SendJob.hpp @@ -22,6 +22,8 @@ class SendJob : public Job PrintPrepareData job_data; std::string m_dev_id; bool m_job_finished{ false }; + bool m_lifecycle_started{ false }; + bool m_lifecycle_finished{ false }; int m_print_job_completed_id = 0; bool m_is_check_mode{false}; bool m_check_and_continue{false}; diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 428f45c845..3493506228 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -29,6 +29,7 @@ #include "libslic3r/Tesselate.hpp" #include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/Utils.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "I18N.hpp" #include "GUI_App.hpp" @@ -2626,11 +2627,20 @@ void PartPlate::set_plate_name(const std::string& name) if (boost::equals(m_name, name)) return; + const std::string previous_name = m_name; m_name = name; if (m_print != nullptr) m_print->set_plate_name(name); invalidate_plate_name_texture(); + + if (m_plater != nullptr && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = name; + ctx.previous_name = previous_name; + ctx.index = m_plate_index; + fire_lifecycle_event(LifecycleEvent::PlateRenamed, ctx); + } } //get the print's object, result and index @@ -4741,9 +4751,16 @@ int PartPlateList::create_plate(bool adjust_position) if (m_plater) { // In GUI mode - wxGetApp().obj_list()->on_plate_added(plate); + wxGetApp().obj_list()->on_plate_added(plate); } + if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = plate->get_plate_name(); + ctx.index = new_index; + fire_lifecycle_event(LifecycleEvent::PlateCreated, ctx); + } + BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(":created a new plate %1%") % new_index; return new_index; } @@ -4842,6 +4859,7 @@ int PartPlateList::delete_plate(int index) BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(":plate %1%, has an invalid index %2%") % index % plate->get_index(); return -1; } + const std::string plate_name = plate->get_plate_name(); if (m_plater) { // In GUI mode @@ -4924,6 +4942,13 @@ int PartPlateList::delete_plate(int index) delete plate; + if (m_plater != nullptr && m_intialized && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = plate_name; + ctx.index = index; + fire_lifecycle_event(LifecycleEvent::PlateDeleted, ctx); + } + // FIX: context of BackgroundSliceProcess and gcode preview need to be updated before ObjectList::reload_all_plates(). #if 0 if (m_plater != nullptr) { @@ -5032,6 +5057,7 @@ int PartPlateList::select_plate(int index) if (m_plate_list.empty() || index >= m_plate_list.size()) { return -1; } + const int previous_index = m_current_plate; // BBS: erase unnecessary snapshot if (get_curr_plate_index() != index && m_intialized) { @@ -5058,6 +5084,13 @@ int PartPlateList::select_plate(int index) //wxQueueEvent(m_plater, new SimpleEvent(EVT_GLCANVAS_PLATE_SELECT)); } + if (previous_index != index && m_intialized && m_plater != nullptr && !m_plater->is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = m_plate_list[index]->get_plate_name(); + ctx.index = index; + fire_lifecycle_event(LifecycleEvent::PlateSelected, ctx); + } + return 0; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 76156011dd..d4386fbe93 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -57,6 +57,7 @@ #include #include "libslic3r/libslic3r.h" +#include "libslic3r/LifecycleEvents.hpp" #include "libslic3r/Format/STL.hpp" #include "libslic3r/Format/DRC.hpp" #include "libslic3r/Format/STEP.hpp" @@ -10264,7 +10265,14 @@ void Plater::priv::remove(size_t obj_idx) view3D->enable_layers_editing(false); m_worker.cancel_all(); + std::string obj_name = (obj_idx < model.objects.size()) ? model.objects[obj_idx]->name : std::to_string(obj_idx); model.delete_object(obj_idx); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = obj_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx); + } //BBS: notify partplate the instance removed partplate_list.notify_instance_removed(obj_idx, -1); update(); @@ -10297,7 +10305,14 @@ bool Plater::priv::delete_object_from_model(size_t obj_idx, bool refresh_immedia if (obj->is_cut()) sidebar->obj_list()->invalidate_cut_info_for_object(obj_idx); + std::string obj_name = obj->name; model.delete_object(obj_idx); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = obj_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ObjectDeleted, ctx); + } //BBS: notify partplate the instance removed partplate_list.notify_instance_removed(obj_idx, -1); @@ -10343,10 +10358,22 @@ void Plater::priv::delete_all_objects_from_model() void Plater::priv::reset(bool apply_presets_change) { + // TakeSnapshot below and load_current_presets() further down each re-evaluate the + // aggregate dirty flag against a baseline that hasn't been reset yet, so they can toggle + // is_dirty() back and forth several times before it settles; coalesce those into one event. + ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(dirty_state); + Plater::TakeSnapshot snapshot(q, _u8L("Reset Project"), UndoRedo::SnapshotType::ProjectSeparator); clear_warnings(); + const std::string closed_project_name = into_u8(get_project_filename()); + if (!closed_project_name.empty() || !model.objects.empty()) { + Slic3r::LifecycleEventContext ctx; + ctx.name = closed_project_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectClosed, ctx); + } // A new project must not inherit the previous project's published selection (Feature A/B). m_has_pending_published = false; m_pending_published_keys.clear(); @@ -12774,11 +12801,15 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) notification_manager->set_slicing_progress_export_possible(); // Reset the "export G-code path" name, so that the automatic background processing will be enabled again. + const std::string lifecycle_job_name = this->background_process.fff_print() ? + this->background_process.fff_print()->output_filename() : std::string(); this->background_process.reset_export(); // This bool stops showing export finished notification even when process_completed_with_error is false bool has_error = false; + std::string lifecycle_error_msg; if (evt.error()) { auto message = evt.format_error_message(); + lifecycle_error_msg = message.first; if (evt.critical_error()) { if (q->m_tracking_popup_menu) { // We don't want to pop-up a message box when tracking a pop-up menu. @@ -12816,6 +12847,14 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt) is_finished = true; } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = lifecycle_job_name; + ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok); + ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string()); + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx); + } + //BBS: set the current plater's slice result to valid if (!this->background_process.empty()) this->background_process.get_current_plate()->update_slice_result_valid_state(evt.success()); @@ -15349,21 +15388,33 @@ int Plater::new_project(bool skip_confirm, bool silent, const wxString& project_ //get_partplate_list().reinit(); //get_partplate_list().update_slice_context_to_current_plate(p->background_process); //p->preview->update_gcode_result(p->partplate_list.get_current_slice_result()); - reset(transfer_preset_changes); - reset_project_dirty_after_save(); - reset_project_dirty_initial_presets(); - wxGetApp().update_saved_preset_from_current_preset(); - update_project_dirty_from_presets(); + if (!silent) { + Slic3r::LifecycleEventContext ctx; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::NewProject, ctx); + } + { + // Same rationale as in Plater::priv::reset(): the whole reset + preset-reload + + // baseline-reset sequence below settles into its final dirty state only once it + // completes, so hold notifications until then to avoid firing on transient flips. + ProjectDirtyStateManager::NotificationSuppressor dirty_notify_suppressor(p->dirty_state); - //reset project - p->project.reset(); - //set project name - if (project_name.empty()) - p->set_project_name(_L("Untitled")); - else - p->set_project_name(project_name); + reset(transfer_preset_changes); + reset_project_dirty_after_save(); + reset_project_dirty_initial_presets(); + wxGetApp().update_saved_preset_from_current_preset(); + update_project_dirty_from_presets(); - Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator); + //reset project + p->project.reset(); + //set project name + if (project_name.empty()) + p->set_project_name(_L("Untitled")); + else + p->set_project_name(project_name); + + Plater::TakeSnapshot snapshot(this, "New Project", UndoRedo::SnapshotType::ProjectSeparator); + } Model m; model().load_from(m); // new id avoid same path name @@ -15481,6 +15532,13 @@ void Plater::load_project(wxString const& filename2, p->set_project_name(_L("Untitled")); } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = into_u8(load_restore ? originfile : filename); + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectOpened, ctx); + } + } else { if (using_exported_file()) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename; @@ -15552,11 +15610,23 @@ int Plater::save_project(bool saveAs) if (full_pathnames) { save_strategy = save_strategy | SaveStrategy::FullPathSources; } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = into_u8(filename); + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectBeforeSave, ctx); + } if (export_3mf(into_path(filename), save_strategy) < 0) { MessageDialog(this, _L("Failed to save the project.\nPlease check whether the folder exists online or if other programs have the project file open."), _L("Save project"), wxOK | wxICON_WARNING).ShowModal(); return wxID_CANCEL; } + { + Slic3r::LifecycleEventContext ctx; + ctx.name = into_u8(filename); + ctx.code = Slic3r::LifecycleEvtCode::Ok; + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::ProjectAfterSave, ctx); + } Slic3r::remove_backup(model(), false); @@ -20889,6 +20959,14 @@ void Plater::changed_object(ModelObject &object){ // Check outside bed get_current_canvas3D()->requires_check_outside_state(); + + if (!is_loading_project()) { + LifecycleEventContext ctx; + ctx.name = object.name; + ctx.id = std::to_string(object.id().id); + ctx.source = "geometry"; + fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx); + } } void Plater::changed_object(int obj_idx) @@ -20925,6 +21003,20 @@ void Plater::changed_objects(const std::vector& object_idxs) // update print this->p->schedule_background_process(); + + if (!is_loading_project()) { + for (size_t obj_idx : object_idxs) { + if (obj_idx >= p->model.objects.size() || p->model.objects[obj_idx] == nullptr) + continue; + + LifecycleEventContext ctx; + ctx.name = p->model.objects[obj_idx]->name; + ctx.id = std::to_string(p->model.objects[obj_idx]->id().id); + ctx.index = static_cast(obj_idx); + ctx.source = "geometry"; + fire_lifecycle_event(LifecycleEvent::ObjectChanged, ctx); + } + } } void Plater::schedule_background_process(bool schedule/* = true*/) diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.cpp b/src/slic3r/GUI/ProjectDirtyStateManager.cpp index c986328f71..53eb3cd353 100644 --- a/src/slic3r/GUI/ProjectDirtyStateManager.cpp +++ b/src/slic3r/GUI/ProjectDirtyStateManager.cpp @@ -6,6 +6,7 @@ #include "MainFrame.hpp" #include "I18N.hpp" #include "Plater.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include @@ -17,13 +18,23 @@ namespace GUI { void ProjectDirtyStateManager::update_from_undo_redo_stack(bool dirty) { + const bool was_dirty = is_dirty(); m_plater_dirty = dirty; + notify_dirty_change(was_dirty, "undo_redo"); if (const Plater *plater = wxGetApp().plater(); plater && wxGetApp().initialized()) wxGetApp().mainframe->update_title(); } +void ProjectDirtyStateManager::set_plater_dirty(bool is_dirty) +{ + const bool was_dirty = this->is_dirty(); + m_plater_dirty = is_dirty; + notify_dirty_change(was_dirty, "plater"); +} + void ProjectDirtyStateManager::update_from_presets() { + const bool was_dirty = is_dirty(); m_presets_dirty = false; // check switching of the presets only for exist/loaded project, but not for new GUI_App &app = wxGetApp(); @@ -45,18 +56,53 @@ void ProjectDirtyStateManager::update_from_presets() } m_presets_dirty |= app.has_unsaved_preset_changes(); m_project_config_dirty = m_initial_project_config != app.preset_bundle->project_config; + notify_dirty_change(was_dirty, "presets"); app.mainframe->update_title(); } void ProjectDirtyStateManager::reset_after_save() { + const bool was_dirty = is_dirty(); this->reset_initial_presets(); m_plater_dirty = false; m_presets_dirty = false; m_project_config_dirty = false; + notify_dirty_change(was_dirty, "save"); wxGetApp().mainframe->update_title(); } +void ProjectDirtyStateManager::notify_dirty_change(bool was_dirty, const char *source) +{ + if (m_suppress_depth > 0) + return; + + const bool dirty = is_dirty(); + if (was_dirty == dirty) + return; + + LifecycleEventContext ctx; + ctx.code = LifecycleEvtCode::Ok; + ctx.dirty = dirty; + ctx.source = source; + fire_lifecycle_event(LifecycleEvent::ProjectDirtyChanged, ctx); +} + +void ProjectDirtyStateManager::begin_suppress_notifications() +{ + if (m_suppress_depth == 0) + m_suppress_entry_dirty = is_dirty(); + ++m_suppress_depth; +} + +void ProjectDirtyStateManager::end_suppress_notifications() +{ + assert(m_suppress_depth > 0); + if (m_suppress_depth > 0) + --m_suppress_depth; + if (m_suppress_depth == 0) + notify_dirty_change(m_suppress_entry_dirty, "batch"); +} + void ProjectDirtyStateManager::reset_initial_presets() { m_initial_presets.fill(std::string{}); @@ -168,4 +214,3 @@ void ProjectDirtyStateManager::render_debug_window() const } // namespace GUI } // namespace Slic3r - diff --git a/src/slic3r/GUI/ProjectDirtyStateManager.hpp b/src/slic3r/GUI/ProjectDirtyStateManager.hpp index 171d19e219..fd121d1d85 100644 --- a/src/slic3r/GUI/ProjectDirtyStateManager.hpp +++ b/src/slic3r/GUI/ProjectDirtyStateManager.hpp @@ -14,21 +14,42 @@ public: void reset_after_save(); void reset_initial_presets(); - void set_plater_dirty(bool is_dirty) { m_plater_dirty = is_dirty; } + void set_plater_dirty(bool is_dirty); bool is_dirty() const { return m_plater_dirty || m_project_config_dirty || m_presets_dirty; } bool is_presets_dirty() const { return m_presets_dirty; } + // RAII guard coalescing dirty-state updates: while any guard is alive, ProjectDirtyChanged + // notifications are held back; when the outermost guard is destroyed, at most one + // notification fires, reflecting only the net change across the whole guarded scope. + class NotificationSuppressor + { + public: + explicit NotificationSuppressor(ProjectDirtyStateManager &owner) : m_owner(owner) { m_owner.begin_suppress_notifications(); } + ~NotificationSuppressor() { m_owner.end_suppress_notifications(); } + NotificationSuppressor(const NotificationSuppressor &) = delete; + NotificationSuppressor &operator=(const NotificationSuppressor &) = delete; + private: + ProjectDirtyStateManager &m_owner; + }; + #if ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW void render_debug_window() const; #endif // ENABLE_PROJECT_DIRTY_STATE_DEBUG_WINDOW private: + void notify_dirty_change(bool was_dirty, const char *source); + void begin_suppress_notifications(); + void end_suppress_notifications(); + // Does the Undo / Redo stack indicate the project is dirty? bool m_plater_dirty { false }; // Do the presets indicate the project is dirty? bool m_presets_dirty { false }; // Is the project config dirty? bool m_project_config_dirty { false }; + // NotificationSuppressor nesting depth and the dirty state observed when the outermost guard began. + int m_suppress_depth { 0 }; + bool m_suppress_entry_dirty { false }; // Keeps track of preset names selected at the time of last project save. std::array m_initial_presets; DynamicPrintConfig m_initial_project_config; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index f6c1cbf901..1312d0e11d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -38,6 +38,7 @@ #include "slic3r/Utils/NetworkAgentFactory.hpp" #include "slic3r/Utils/PresetUpdater.hpp" #include "slic3r/plugin/PluginConfig.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "Plater.hpp" #include "MainFrame.hpp" #include "format.hpp" @@ -6964,6 +6965,20 @@ bool Tab::select_preset( } load_current_preset(); + { + Slic3r::LifecycleEventContext ctx; + ctx.name = preset_name; + ctx.code = Slic3r::LifecycleEvtCode::Ok; + switch (m_type) { + case Preset::TYPE_PRINT: ctx.msg = "print"; break; + case Preset::TYPE_SLA_PRINT: ctx.msg = "sla_print"; break; + case Preset::TYPE_FILAMENT: ctx.msg = "filament"; break; + case Preset::TYPE_SLA_MATERIAL: ctx.msg = "sla_material"; break; + case Preset::TYPE_PRINTER: ctx.msg = "printer"; break; + default: break; + } + Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::PresetSelected, ctx); + } if (delete_third_printer) { wxGetApp().CallAfter([filament_presets, process_presets]() { diff --git a/src/slic3r/GUI/TaskManager.cpp b/src/slic3r/GUI/TaskManager.cpp index d294d140c5..64f3e5d6a5 100644 --- a/src/slic3r/GUI/TaskManager.cpp +++ b/src/slic3r/GUI/TaskManager.cpp @@ -1,10 +1,13 @@ #include "TaskManager.hpp" #include "libslic3r/Thread.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "nlohmann/json.hpp" #include "MainFrame.hpp" #include "GUI_App.hpp" +#include + using namespace nlohmann; namespace Slic3r { @@ -214,17 +217,34 @@ int TaskManager::schedule(TaskStateInfo* task) boost::thread* new_sending_thread = new boost::thread(); *new_sending_thread = Slic3r::create_thread( [this, task] { + // Keep both lifecycle callbacks on this per-task worker thread. Plugin observers can + // therefore associate Started and Finished for one task with a single execution context. + LifecycleEventContext start_ctx; + start_ctx.name = task->params().project_name; + start_ctx.device_id = task->params().dev_id; + start_ctx.job_id = std::to_string(task->task_info_id); + start_ctx.source = "task_manager"; + fire_lifecycle_event(LifecycleEvent::PrintJobStarted, start_ctx); + + int result = -1; if (!m_agent) { BOOST_LOG_TRIVIAL(trace) << "task_manager: NetworkAgent is nullptr"; - return; } - assert(m_agent); + else { + assert(m_agent); + try { // DEBUG FOR TEST #if 0 - int result = start_print_test(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn); + result = start_print_test(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn); #else - int result = m_agent->start_print(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn); + result = m_agent->start_print(task->get_params(), task->update_status_fn, task->cancel_fn, task->wait_fn); #endif + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(error) << "task_manager: start_print threw: " << ex.what(); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "task_manager: start_print threw an unknown exception"; + } + } if (result == 0) { last_sent_timestamp = std::chrono::system_clock::now(); task->set_sent_time(last_sent_timestamp); @@ -237,6 +257,16 @@ int TaskManager::schedule(TaskStateInfo* task) task->set_state(TaskState::TS_SEND_CANCELED); } } + + LifecycleEventContext finish_ctx; + finish_ctx.name = task->params().project_name; + finish_ctx.device_id = task->params().dev_id; + finish_ctx.job_id = std::to_string(task->task_info_id); + finish_ctx.source = "task_manager"; + finish_ctx.code = result == 0 ? LifecycleEvtCode::Ok : + (task->is_canceled() ? LifecycleEvtCode::Warn : LifecycleEvtCode::Error); + finish_ctx.msg = result == 0 ? "" : (task->is_canceled() ? "cancelled" : "failed"); + fire_lifecycle_event(LifecycleEvent::PrintJobFinished, finish_ctx); /* remove from sending task list */ m_scedule_mutex.lock(); diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 37e0a41f83..04709fef4c 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -24,6 +24,7 @@ #include "CrealityPrint.hpp" #include "../GUI/PrintHostDialogs.hpp" #include "../GUI/MainFrame.hpp" +#include "slic3r/plugin/PluginManager.hpp" #include "Obico.hpp" #include "Flashforge.hpp" #include "SimplyPrint.hpp" @@ -360,12 +361,29 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job) { emit_progress(0); // Indicate the upload is starting + // Captured before upload_data is moved into upload() below. + const std::string upload_filename = the_job.upload_data.source_path.filename().string(); + + { + LifecycleEventContext ctx; + ctx.name = upload_filename; + ctx.code = LifecycleEvtCode::Ok; + fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx); + } + bool success = the_job.printhost->upload(std::move(the_job.upload_data), [this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); }, [this](wxString error) { this->error_fn(std::move(error)); }, [this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); } ); + { + LifecycleEventContext ctx; + ctx.name = upload_filename; + ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error; + fire_lifecycle_event(LifecycleEvent::UploadFinished, ctx); + } + if (success) { emit_progress(100); if (the_job.switch_to_device_tab) { diff --git a/src/slic3r/plugin/PluginHooks.cpp b/src/slic3r/plugin/PluginHooks.cpp index c6aaaf803c..2d84df5fdb 100644 --- a/src/slic3r/plugin/PluginHooks.cpp +++ b/src/slic3r/plugin/PluginHooks.cpp @@ -7,6 +7,7 @@ #include "libslic3r/Config.hpp" #include "libslic3r/Exception.hpp" +#include "libslic3r/LifecycleEvents.hpp" #include "libslic3r/Print.hpp" #include "libslic3r_version.h" @@ -46,6 +47,16 @@ void install_capability_resolver() }); } +// Global libslic3r-side seam (Slic3r::fire_lifecycle_event, in libslic3r/LifecycleEvents.hpp): +// broadcasts to every loaded, enabled capability regardless of type, unlike the SlicingPipeline +// hook below which only targets picker-selected SlicingPipeline capabilities. +void install_lifecycle_event_hook() +{ + set_lifecycle_hook_fn([](LifecycleEvent event, const LifecycleEventContext& ctx) { + PluginManager::instance().dispatch_lifecycle_event(event, ctx); + }); +} + // Print::process() fires this hook at each pipeline seam on the slicing worker // thread; here we run the picker-selected SlicingPipeline capabilities. Per // capability we acquire the GIL, honor cancellation, and convert a plugin @@ -122,12 +133,14 @@ void install() { install_capability_resolver(); install_slicing_pipeline_hook(); + install_lifecycle_event_hook(); } void uninstall() { ConfigBase::set_resolve_capability_fn(nullptr); Print::set_slicing_pipeline_hook_fn(nullptr); + set_lifecycle_hook_fn(nullptr); } } // namespace Slic3r::plugin_hooks diff --git a/src/slic3r/plugin/PluginHooks.hpp b/src/slic3r/plugin/PluginHooks.hpp index bff276c5df..5eeacfa449 100644 --- a/src/slic3r/plugin/PluginHooks.hpp +++ b/src/slic3r/plugin/PluginHooks.hpp @@ -14,8 +14,9 @@ namespace Slic3r::plugin_hooks { void install(); // Reset every hook to null so none can enter Python after the interpreter -// finalizes. Called from PluginManager::shutdown(); callers must have stopped -// background slicing first (resetting a hook while process() runs is a race). +// finalizes. The lifecycle-event hook drains callbacks already in progress +// before returning. Other hooks retain their existing caller-side shutdown +// requirements. void uninstall(); } // namespace Slic3r::plugin_hooks diff --git a/src/slic3r/plugin/PluginManager.cpp b/src/slic3r/plugin/PluginManager.cpp index 57bce7d2f2..022f85eb1f 100644 --- a/src/slic3r/plugin/PluginManager.cpp +++ b/src/slic3r/plugin/PluginManager.cpp @@ -1,5 +1,6 @@ #include "PluginManager.hpp" +#include #include #include #include @@ -134,7 +135,8 @@ void PluginManager::shutdown() BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": PluginManager shutdown enter"; // Detach the libslic3r hooks first so nothing dispatches into Python while (or after) plugins - // unload. Callers stop background slicing before this. + // unload. The lifecycle-event hook also drains callbacks already in progress before returning; + // the remaining hook seams retain their existing shutdown requirements. plugin_hooks::uninstall(); // Reject new plugin loads before we drain. @@ -2089,4 +2091,33 @@ ExecutionResult PluginManager::run_script_capability(const std::string& plugin_k return result; } +void PluginManager::dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx) { + const auto is_canceled = [&ctx]() { + return ctx.cancellation_check && ctx.cancellation_check(); + }; + + if (is_canceled()) + return; + + for (const auto& cap : get_plugin_capabilities()) { + if (!cap || !cap->is_enabled()) continue; + if (is_canceled()) + break; + try { + cap->on_lifecycle_event(evt, ctx); + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name() + << "' on_lifecycle_event(" << lifecycle_event_to_string(evt) << ") threw: " << ex.what() + << " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code) + << ", msg='" << ctx.msg << "']"; + } catch (...) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": plugin '" << cap->audit_plugin_key() << "/" << cap->name() + << "' on_lifecycle_event(" << lifecycle_event_to_string(evt) + << ") threw a non-standard exception" + << " [ctx name='" << ctx.name << "', code=" << lifecycle_evt_code_to_string(ctx.code) + << ", msg='" << ctx.msg << "']"; + } + } +} + } // namespace Slic3r diff --git a/src/slic3r/plugin/PluginManager.hpp b/src/slic3r/plugin/PluginManager.hpp index e1fedaff94..891f787bf2 100644 --- a/src/slic3r/plugin/PluginManager.hpp +++ b/src/slic3r/plugin/PluginManager.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -20,7 +21,6 @@ #include #include "CloudPluginService.hpp" -#include "PluginFsUtils.hpp" #include "PluginDescriptor.hpp" #include "PluginLoader.hpp" #include "PluginConfig.hpp" @@ -210,6 +210,8 @@ public: ExecutionResult run_script_capability(const std::string& plugin_key, const std::string& capability_name, std::string& error); + void dispatch_lifecycle_event(LifecycleEvent evt, const LifecycleEventContext& ctx); + private: PluginManager() = default; PluginManager(const PluginManager&) = delete; diff --git a/src/slic3r/plugin/PyPluginTrampoline.hpp b/src/slic3r/plugin/PyPluginTrampoline.hpp index ad7fe25f10..4c77f1ba9c 100644 --- a/src/slic3r/plugin/PyPluginTrampoline.hpp +++ b/src/slic3r/plugin/PyPluginTrampoline.hpp @@ -132,6 +132,11 @@ public: { ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_cancelled); } + + void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) override + { + ORCA_PY_OVERRIDE_AUDITED([] {}, PYBIND11_OVERRIDE, void, Base, on_lifecycle_event, event, ctx); + } }; class PyPluginInterfaceTrampoline : public PyPluginCommonTrampoline diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index 11bb6159cc..a88695005c 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -377,6 +377,61 @@ void bind_python_api(pybind11::module_& m) .value("FatalError", PluginResult::FatalError) .export_values(); + py::enum_(m, "LifecycleEvent", "Application lifecycle moment passed to on_lifecycle_event") + .value("NewProject", LifecycleEvent::NewProject) + .value("ProjectOpened", LifecycleEvent::ProjectOpened) + .value("ProjectBeforeSave", LifecycleEvent::ProjectBeforeSave) + .value("ProjectAfterSave", LifecycleEvent::ProjectAfterSave) + .value("ProjectClosed", LifecycleEvent::ProjectClosed) + .value("ProjectDirtyChanged", LifecycleEvent::ProjectDirtyChanged) + .value("SliceStarted", LifecycleEvent::SliceStarted) + .value("SliceGeometryFinished", LifecycleEvent::SliceGeometryFinished) + .value("GCodeExportStarted", LifecycleEvent::GCodeExportStarted) + .value("GCodeExportFinished", LifecycleEvent::GCodeExportFinished) + .value("SlicingJobComplete", LifecycleEvent::SlicingJobComplete) + .value("ObjectAdded", LifecycleEvent::ObjectAdded) + .value("ObjectDeleted", LifecycleEvent::ObjectDeleted) + .value("ObjectTransformed", LifecycleEvent::ObjectTransformed) + .value("ObjectChanged", LifecycleEvent::ObjectChanged) + .value("ObjectRenamed", LifecycleEvent::ObjectRenamed) + .value("PlateCreated", LifecycleEvent::PlateCreated) + .value("PlateDeleted", LifecycleEvent::PlateDeleted) + .value("PlateSelected", LifecycleEvent::PlateSelected) + .value("PlateRenamed", LifecycleEvent::PlateRenamed) + .value("PresetSelected", LifecycleEvent::PresetSelected) + .value("PresetSaved", LifecycleEvent::PresetSaved) + .value("PrintStateChanged", LifecycleEvent::PrintStateChanged) + .value("DeviceOnline", LifecycleEvent::DeviceOnline) + .value("DeviceOffline", LifecycleEvent::DeviceOffline) + .value("DeviceDiscovered", LifecycleEvent::DeviceDiscovered) + .value("DeviceSelected", LifecycleEvent::DeviceSelected) + .value("UploadStarted", LifecycleEvent::UploadStarted) + .value("UploadFinished", LifecycleEvent::UploadFinished) + .value("PrintJobStarted", LifecycleEvent::PrintJobStarted) + .value("PrintJobFinished", LifecycleEvent::PrintJobFinished) + .value("SendJobStarted", LifecycleEvent::SendJobStarted) + .value("SendJobFinished", LifecycleEvent::SendJobFinished) + .export_values(); + + py::enum_(m, "LifecycleEvtCode", "Outcome code accompanying a LifecycleEventContext") + .value("Ok", LifecycleEvtCode::Ok) + .value("Error", LifecycleEvtCode::Error) + .value("Warn", LifecycleEvtCode::Warn) + .export_values(); + + py::class_(m, "LifecycleEventContext", "Payload accompanying a LifecycleEvent") + .def(py::init<>()) + .def_readonly("name", &LifecycleEventContext::name) + .def_readonly("code", &LifecycleEventContext::code) + .def_readonly("msg", &LifecycleEventContext::msg) + .def_readonly("id", &LifecycleEventContext::id) + .def_readonly("previous_name", &LifecycleEventContext::previous_name) + .def_readonly("device_id", &LifecycleEventContext::device_id) + .def_readonly("job_id", &LifecycleEventContext::job_id) + .def_readonly("source", &LifecycleEventContext::source) + .def_readonly("index", &LifecycleEventContext::index) + .def_readonly("dirty", &LifecycleEventContext::dirty); + py::class_(m, "PluginContext", "Context shared with plugin entry points") .def(py::init<>()) .def_readwrite("orca_version", &PluginContext::orca_version); @@ -401,6 +456,9 @@ void bind_python_api(pybind11::module_& m) .def("get_type", &PluginCapabilityInterface::get_type) .def("on_load", &PluginCapabilityInterface::on_load) .def("on_unload", &PluginCapabilityInterface::on_unload) + .def("on_lifecycle_event", &PluginCapabilityInterface::on_lifecycle_event, + "Override to react to an application lifecycle moment (LifecycleEvent) and its\n" + "LifecycleEventContext payload. Available on every capability type.") .def("has_config_ui", &PluginCapabilityInterface::has_config_ui, "Override to return True to replace the host's default JSON editor with your own HTML\n" "UI, returned by get_config_ui(). Every capability is configurable and appears in the\n" diff --git a/src/slic3r/plugin/PythonPluginInterface.hpp b/src/slic3r/plugin/PythonPluginInterface.hpp index 8b7518cf1c..2702275166 100644 --- a/src/slic3r/plugin/PythonPluginInterface.hpp +++ b/src/slic3r/plugin/PythonPluginInterface.hpp @@ -10,6 +10,8 @@ #include #include +#include + namespace Slic3r { enum class PluginCapabilityType { PrinterConnection = 0, Pages, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown }; @@ -167,6 +169,8 @@ public: virtual void on_unload() {} virtual void on_cancelled() {} + virtual void on_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx) { (void) event; (void) ctx; } + // ── C++-only host state, never exposed to Python. Set by the loader at materialization. ── // // The capability owns its own identity and enable flag: they are read once under the GIL, live diff --git a/tests/slic3rutils/test_plugin_lifecycle.cpp b/tests/slic3rutils/test_plugin_lifecycle.cpp index 63a4e6827b..bac3a19ea9 100644 --- a/tests/slic3rutils/test_plugin_lifecycle.cpp +++ b/tests/slic3rutils/test_plugin_lifecycle.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -12,8 +13,11 @@ #include #include +#include #include +#include #include +#include #include using namespace Slic3r; @@ -114,6 +118,72 @@ PluginDescriptor descriptor_of(PluginManager& manager, const std::string& plugin } // namespace +TEST_CASE("Lifecycle hook shutdown drains concurrent dispatch", "[PluginLifecycle][LifecycleEvents]") +{ + std::mutex mutex; + std::condition_variable cv; + bool callback_entered = false; + bool release_callback = false; + bool shutdown_started = false; + bool shutdown_finished = false; + + set_lifecycle_hook_fn([&](LifecycleEvent, const LifecycleEventContext&) { + std::unique_lock lock(mutex); + callback_entered = true; + cv.notify_all(); + cv.wait(lock, [&] { return release_callback; }); + }); + + std::thread dispatch_thread([] { + LifecycleEventContext ctx; + fire_lifecycle_event(LifecycleEvent::ProjectOpened, ctx); + }); + + bool callback_was_entered = false; + { + std::unique_lock lock(mutex); + callback_was_entered = cv.wait_for(lock, std::chrono::seconds(5), [&] { return callback_entered; }); + } + if (!callback_was_entered) { + { + std::lock_guard lock(mutex); + release_callback = true; + cv.notify_all(); + } + dispatch_thread.join(); + set_lifecycle_hook_fn(nullptr); + FAIL("Lifecycle callback did not start"); + } + + std::thread shutdown_thread([&] { + { + std::lock_guard lock(mutex); + shutdown_started = true; + cv.notify_all(); + } + set_lifecycle_hook_fn(nullptr); + std::lock_guard lock(mutex); + shutdown_finished = true; + cv.notify_all(); + }); + + bool shutdown_was_started = false; + { + std::unique_lock lock(mutex); + shutdown_was_started = cv.wait_for(lock, std::chrono::seconds(5), [&] { return shutdown_started; }); + if (shutdown_was_started) + CHECK_FALSE(cv.wait_for(lock, std::chrono::milliseconds(100), [&] { return shutdown_finished; })); + release_callback = true; + cv.notify_all(); + } + + dispatch_thread.join(); + shutdown_thread.join(); + if (!shutdown_was_started) + FAIL("Lifecycle hook shutdown did not start"); + CHECK(shutdown_finished); +} + TEST_CASE("A discovered script plugin loads and materializes its capability", "[PluginLifecycle][Python]") { ScopedPluginManager plugin_system;