fix: snapshot Moonraker connection state and make agent teardown responsive

This commit is contained in:
Ian Chua
2026-10-07 16:58:13 +08:00
parent 5e3a526b72
commit 5b81536be2
7 changed files with 95 additions and 43 deletions
+5 -3
View File
@@ -294,7 +294,9 @@ bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMod
if (sync_mode != get_filament_sync_mode())
return false;
if (device_info.dev_ip.empty()) {
const MoonrakerDeviceInfo info = snapshot_device_info();
if (info.dev_ip.empty()) {
BOOST_LOG_TRIVIAL(warning)
<< "CrealityPrintAgent::fetch_filament_info: no device IP, falling back to base agent";
return MoonrakerPrinterAgent::fetch_filament_info(std::move(dev_id));
@@ -303,11 +305,11 @@ bool CrealityPrintAgent::fetch_filament_info(std::string dev_id, FilamentSyncMod
// Build a CrealityPrint helper so we can use its model detection + WS helpers
// (added in upstream PR #13291).
DynamicPrintConfig cfg;
cfg.set_key_value("print_host", new ConfigOptionString("http://" + device_info.dev_ip));
cfg.set_key_value("print_host", new ConfigOptionString("http://" + info.dev_ip));
cfg.set_key_value("print_host_webui", new ConfigOptionString(""));
cfg.set_key_value("printhost_cafile", new ConfigOptionString(""));
cfg.set_key_value("printhost_port", new ConfigOptionString(""));
cfg.set_key_value("printhost_apikey", new ConfigOptionString(device_info.api_key));
cfg.set_key_value("printhost_apikey", new ConfigOptionString(info.api_key));
cfg.set_key_value("printhost_ssl_ignore_revoke", new ConfigOptionBool(false));
CrealityPrint host(&cfg);
+53 -21
View File
@@ -343,8 +343,9 @@ void MoonrakerPrinterAgent::shutdown()
cmd_thread.join();
}
while (filament_fetch_in_flight.load() > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
{
std::unique_lock<std::mutex> lock(fetch_lifecycle_mutex);
fetch_done_cv.wait(lock, [this] { return filament_fetch_in_flight.load() == 0; });
}
// Fetches read device_info without the lock; clear it once none can run.
@@ -766,8 +767,9 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
queue_fn = queue_on_main_fn;
}
std::string dev_id = device_info.dev_id;
std::string model_id = device_info.model_id;
const MoonrakerDeviceInfo info = snapshot_device_info();
std::string dev_id = info.dev_id;
std::string model_id = info.model_id;
auto apply = [dev_id, model_id, ams_count, max_lane_index, trays]() {
// Look up MachineObject via DeviceManager
@@ -1641,6 +1643,21 @@ MoonrakerPrinterAgent::ConnectionSettings MoonrakerPrinterAgent::get_connection_
return connection;
}
MoonrakerPrinterAgent::MoonrakerDeviceInfo MoonrakerPrinterAgent::snapshot_device_info() const
{
std::lock_guard<std::recursive_mutex> lock(connect_mutex);
return device_info;
}
void MoonrakerPrinterAgent::release_fetch_slot() noexcept
{
{
std::lock_guard<std::mutex> lock(fetch_lifecycle_mutex);
filament_fetch_in_flight.fetch_sub(1, std::memory_order_relaxed);
}
fetch_done_cv.notify_all();
}
void MoonrakerPrinterAgent::configure_http(Http& http, const ConnectionSettings& connection) const
{
http.tls_verify(connection.use_ssl);
@@ -1924,6 +1941,11 @@ bool MoonrakerPrinterAgent::fetch_webcam_info(const ConnectionSettings& connecti
for (const auto& webcam : result["webcams"]) {
if (webcam.is_object())
{
// /server/webcams/list returns disabled webcams too; skip them.
if (webcam.contains("enabled") && webcam["enabled"].is_boolean() &&
!webcam["enabled"].get<bool>()) {
continue;
}
if (webcam.contains("stream_url") && webcam["stream_url"].is_string() &&
!webcam["stream_url"].get<std::string>().empty()) {
camera_url = webcam["stream_url"].get<std::string>();
@@ -1948,7 +1970,9 @@ bool MoonrakerPrinterAgent::fetch_webcam_info(const ConnectionSettings& connecti
}
if (error.empty()) {
if (camera_url.rfind("http", 0) != 0 && !camera_url.empty() && camera_url.front() == '/') {
if (camera_url.rfind("rtsp://", 0) == 0 || camera_url.rfind("rtsps://", 0) == 0) {
stream_mode = CameraStreamMode::rtsp;
} else if (camera_url.rfind("http", 0) != 0 && !camera_url.empty() && camera_url.front() == '/') {
// why: Moonraker's API port serves a JSON 404 for /webcam; relative camera URLs use the printer web root.
const size_t scheme_end = connection.base_url.find("://");
const size_t authority_start = scheme_end == std::string::npos ? 0 : scheme_end + 3;
@@ -2202,7 +2226,7 @@ int MoonrakerPrinterAgent::send_version_info(const std::string& dev_id)
nlohmann::json module;
module["name"] = "ota";
module["sw_ver"] = device_info.version;
module["sw_ver"] = snapshot_device_info().version;
module["product_name"] = "Moonraker";
payload["info"]["module"].push_back(module);
@@ -2214,7 +2238,7 @@ int MoonrakerPrinterAgent::send_access_code(const std::string& dev_id)
{
nlohmann::json payload;
payload["system"]["command"] = "get_access_code";
payload["system"]["access_code"] = device_info.api_key;
payload["system"]["access_code"] = snapshot_device_info().api_key;
dispatch_message(dev_id, payload.dump());
return BAMBU_NETWORK_SUCCESS;
}
@@ -2336,13 +2360,14 @@ void MoonrakerPrinterAgent::stop_status_stream()
{
ws_stop.store(true);
{
// Wake a blocked synchronous ws.read()/ws.write() in run_status_stream();
// ws_stop by itself is only observed between reads.
// Wake a blocked synchronous ws.connect()/tls_handshake()/read()/write() in
// run_status_stream(); ws_stop by itself is only observed between reads.
std::lock_guard<std::mutex> lock(ws_abort_mutex);
if (ws_abort_io) {
ws_abort_io();
}
}
ws_wait_cv.notify_all();
if (ws_thread.joinable()) {
ws_thread.join();
}
@@ -2366,15 +2391,14 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, ConnectionSett
try {
MoonrakerWebsocket ws{endpoint.secure, connection.api_key, connection.ca_file};
ws.connect(endpoint.host, endpoint.port, std::chrono::seconds(10));
ws.tls_handshake(endpoint.host);
// Allow stop_status_stream() to force this socket shut so a blocked
// synchronous ws.read()/ws.write() returns with an error (Beast's
// expires_after() does not bound synchronous operations). Declared
// after `ws` so the hook is cleared before `ws` is destroyed on every
// exit path (fallthrough, break, exception); ws_abort_mutex keeps the
// hook from running against a half-destroyed `ws`.
// Allow stop_status_stream() to force this socket shut so a blocked synchronous
// ws.connect()/tls_handshake()/read()/write() returns with an error (Beast's
// expires_after() does not bound synchronous operations). Registered before
// connect() so a stalled handshake is interruptible too; declared after `ws`
// so the hook is cleared before `ws` is destroyed on every exit path
// (fallthrough, break, exception); ws_abort_mutex keeps the hook from running
// against a half-destroyed `ws`.
ScopeGuard ws_abort_guard([this] {
std::lock_guard<std::mutex> lock(ws_abort_mutex);
ws_abort_io = nullptr;
@@ -2386,6 +2410,9 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, ConnectionSett
};
}
ws.connect(endpoint.host, endpoint.port, std::chrono::seconds(10));
ws.tls_handshake(endpoint.host);
std::string host_header = endpoint.host;
if (!endpoint.port.empty() && endpoint.port != (endpoint.secure ? "7130" : "7125")) {
host_header += ":" + endpoint.port;
@@ -2536,9 +2563,13 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, ConnectionSett
}
// Exponential backoff before reconnection
int delay_ms = base_delay_ms * (1 << std::min(retry_count, 5));
const int delay_ms = base_delay_ms * (1 << std::min(retry_count, 5));
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: Reconnecting in " << delay_ms << "ms (attempt " << (retry_count + 1) << ")";
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
{
// Interruptible so stop_status_stream() doesn't have to wait out a backoff of up to 32 s.
std::unique_lock<std::mutex> wait_lock(ws_wait_mutex);
ws_wait_cv.wait_for(wait_lock, std::chrono::milliseconds(delay_ms), [this] { return ws_stop.load(); });
}
retry_count++;
}
@@ -2831,8 +2862,9 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const
// MachineObject::parse_json routes nozzle_diameter through the legacy nozzle
// parser only when nozzle_type is present as well. Moonraker/Klipper exposes
// the diameter but not Bambu's nozzle type, so use the parser's neutral value.
if (device_info.nozzle_diameter > 0.0f) {
payload["print"]["nozzle_diameter"] = device_info.nozzle_diameter;
const float nozzle_diameter = snapshot_device_info().nozzle_diameter;
if (nozzle_diameter > 0.0f) {
payload["print"]["nozzle_diameter"] = nozzle_diameter;
payload["print"]["nozzle_type"] = "N/A";
}
@@ -109,6 +109,10 @@ public:
CameraStreamMode get_camera_stream_mode() const override;
std::string get_camera_url() const override;
// Called by the fetch-thread RAII guard when a background filament fetch finishes.
// Serialized with the reservation so shutdown()'s wait cannot miss the transition to 0.
void release_fetch_slot() noexcept;
protected:
struct ConnectionSettings
{
@@ -153,6 +157,9 @@ protected:
virtual bool init_device_info(const PrinterConnectionParams& params);
virtual bool fetch_device_info(const ConnectionSettings& connection, MoonrakerDeviceInfo& info, std::string& error) const;
ConnectionSettings get_connection_settings() const;
// Copy of the mutable connection state, taken under connect_mutex. Background threads
// must use this instead of reading device_info directly.
MoonrakerDeviceInfo snapshot_device_info() const;
void configure_http(Http& http, const ConnectionSettings& connection) const;
static float parse_nozzle_diameter(const nlohmann::json& response);
@@ -168,6 +175,7 @@ protected:
// Serializes the shutting_down check with the in-flight reservation.
std::mutex fetch_lifecycle_mutex;
std::condition_variable fetch_done_cv; // notified when filament_fetch_in_flight reaches 0
// Helpers
bool is_numeric(const std::string& value);
@@ -316,6 +324,10 @@ private:
std::mutex ws_abort_mutex;
std::function<void()> ws_abort_io; // guarded by ws_abort_mutex
// Interrupts the exponential reconnect backoff in run_status_stream() when stopping.
std::mutex ws_wait_mutex;
std::condition_variable ws_wait_cv;
// AMS/filament refresh cadence, independent of telemetry dispatch so a steady
// stream of status updates can't starve it (ws_last_emit_ms is reset by those).
static constexpr uint64_t AMS_REFRESH_INTERVAL_MS = 10000;
+10 -9
View File
@@ -41,16 +41,16 @@ bool has_visible_base_preset(const PresetCollection& filaments, const std::strin
return false;
}
// RAII decrement for the in-flight fetch count; movable so a failed thread start still releases it.
// RAII release of the in-flight fetch slot; movable so a failed thread start still releases it.
struct InFlightGuard
{
std::atomic<int>* counter;
explicit InFlightGuard(std::atomic<int>& c) noexcept : counter(&c) {}
InFlightGuard(InFlightGuard&& other) noexcept : counter(other.counter) { other.counter = nullptr; }
MoonrakerPrinterAgent* owner;
explicit InFlightGuard(MoonrakerPrinterAgent& o) noexcept : owner(&o) {}
InFlightGuard(InFlightGuard&& other) noexcept : owner(other.owner) { other.owner = nullptr; }
InFlightGuard(const InFlightGuard&) = delete;
InFlightGuard& operator=(const InFlightGuard&) = delete;
InFlightGuard& operator=(InFlightGuard&&) = delete;
~InFlightGuard() { if (counter) counter->fetch_sub(1, std::memory_order_relaxed); }
~InFlightGuard() { if (owner) owner->release_fetch_slot(); }
};
// nlohmann::json::value() returns the default only when the key is absent; a present but
@@ -104,7 +104,7 @@ bool QidiPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
}
InFlightGuard guard{filament_fetch_in_flight};
InFlightGuard guard{*this};
std::thread([this, guard = std::move(guard), connection = std::move(connection), model_id, model_name]() mutable {
try {
std::string error;
@@ -155,8 +155,9 @@ bool QidiPrinterAgent::apply_box_mapping(const PrintParams& params) const
// job actually routes filament through it. (See qidi-ams-findings.md §2/§8.3 —
// if firmware treats enable_box as "a box exists" rather than "use it this job",
// switch this gate to HasAms()/box_count instead.)
const int enable = params.task_use_ams ? 1 : 0;
if (!send_gcode(device_info.dev_id, "SAVE_VARIABLE VARIABLE=enable_box VALUE=" + std::to_string(enable))) {
const int enable = params.task_use_ams ? 1 : 0;
const std::string dev_id = get_connection_settings().dev_id;
if (!send_gcode(dev_id, "SAVE_VARIABLE VARIABLE=enable_box VALUE=" + std::to_string(enable))) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set enable_box";
return false;
}
@@ -188,7 +189,7 @@ bool QidiPrinterAgent::apply_box_mapping(const PrintParams& params) const
continue; // unmapped filament — skip
const std::string gcode = "SAVE_VARIABLE VARIABLE=value_t" + std::to_string(tool) +
" VALUE=\"'slot" + std::to_string(slot) + "'\"";
if (!send_gcode(device_info.dev_id, gcode)) {
if (!send_gcode(dev_id, gcode)) {
BOOST_LOG_TRIVIAL(error) << "QidiPrinterAgent::apply_box_mapping: failed to set value_t" << tool;
return false;
}
+11 -6
View File
@@ -35,16 +35,16 @@ int64_t now_ms()
std::chrono::steady_clock::now().time_since_epoch()).count();
}
// RAII decrement for the in-flight fetch count; movable so a failed thread start still releases it.
// RAII release of the in-flight fetch slot; movable so a failed thread start still releases it.
struct InFlightGuard
{
std::atomic<int>* counter;
explicit InFlightGuard(std::atomic<int>& c) noexcept : counter(&c) {}
InFlightGuard(InFlightGuard&& other) noexcept : counter(other.counter) { other.counter = nullptr; }
MoonrakerPrinterAgent* owner;
explicit InFlightGuard(MoonrakerPrinterAgent& o) noexcept : owner(&o) {}
InFlightGuard(InFlightGuard&& other) noexcept : owner(other.owner) { other.owner = nullptr; }
InFlightGuard(const InFlightGuard&) = delete;
InFlightGuard& operator=(const InFlightGuard&) = delete;
InFlightGuard& operator=(InFlightGuard&&) = delete;
~InFlightGuard() { if (counter) counter->fetch_sub(1, std::memory_order_relaxed); }
~InFlightGuard() { if (owner) owner->release_fetch_slot(); }
};
// nlohmann::json::value() returns the default only when the key is absent; a present but
@@ -234,7 +234,7 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed);
}
InFlightGuard guard{filament_fetch_in_flight};
InFlightGuard guard{*this};
std::thread([this, guard = std::move(guard), connection]() {
try {
const std::string url = join_url(connection.base_url, "/printer/objects/query?print_task_config&filament_detect");
@@ -368,6 +368,11 @@ bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
return true;
}
std::string SnapmakerPrinterAgent::get_camera_url() const
{
return get_connection_settings().base_url + "/server/files/camera/monitor.jpg";
}
FilamentSyncMode SnapmakerPrinterAgent::get_filament_sync_mode() const
{
if (GUI::wxGetApp().app_config->get_bool("use_printer_agents"))
+1 -1
View File
@@ -22,7 +22,7 @@ public:
FilamentSyncMode get_filament_sync_mode() const override;
int command_start_camera(std::string dev_id) override;
CameraStreamMode get_camera_stream_mode() const override { return CameraStreamMode::http_snapshot; }
std::string get_camera_url() const override { return device_info.base_url + "/server/files/camera/monitor.jpg"; }
std::string get_camera_url() const override;
private:
// Combine filament_type + filament_sub_type into a unified type string
+3 -3
View File
@@ -219,9 +219,9 @@ public:
std::thread([this, finish = std::move(allow_finish_p)] {
struct InFlightGuard
{
std::atomic<int>& counter;
~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); }
} guard{filament_fetch_in_flight};
MoonrakerPrinterAgent& owner;
~InFlightGuard() { owner.release_fetch_slot(); }
} guard{*this};
g_deferred_fetch_running.fetch_add(1, std::memory_order_relaxed);
finish->get_future().wait();