From 847807adac353ca5ad9afa5bca2a773fbb1a6285 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 10 Jun 2026 22:23:12 +0800 Subject: [PATCH] fix: tombstone resolution for 409 status code with error code -3 (#14116) * fix: tombstone resolution for 409 status code with error code -3 * fix: add resolution for undefined conflicts * fix: generate setting id if it is empty for 409 tombstone * fix: force push empty setting_id preset on 409 tombstone * clearner solution --- src/slic3r/GUI/GUI_App.cpp | 53 ++++++++++++++++++---- src/slic3r/GUI/NotificationManager.cpp | 5 +- src/slic3r/GUI/NotificationManager.hpp | 6 ++- src/slic3r/Utils/OrcaCloudServiceAgent.cpp | 50 ++++++++++++-------- src/slic3r/Utils/OrcaCloudServiceAgent.hpp | 2 + 5 files changed, 86 insertions(+), 30 deletions(-) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 8a5897622b..53c987fabb 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -4940,6 +4940,7 @@ void GUI_App::on_http_error(wxCommandEvent &evt) // Parse the conflict body to extract the error code and server profile id int conflict_code = 0; std::string conflict_setting_id; + std::string conflict_preset_name; try { json conflict_body = json::parse(body_str); if (conflict_body.contains("code")) @@ -4947,21 +4948,40 @@ void GUI_App::on_http_error(wxCommandEvent &evt) if (conflict_body.contains("server_profile") && conflict_body["server_profile"].contains("id") && conflict_body["server_profile"]["id"].is_string()) conflict_setting_id = conflict_body["server_profile"]["id"].get(); + // The local preset name is injected into the conflict body by the agent (sync_push), + // since the server response itself omits it for tombstone (-3) conflicts. + if (conflict_body.contains("name") && conflict_body["name"].is_string()) + conflict_preset_name = conflict_body["name"].get(); } catch (...) { BOOST_LOG_TRIVIAL(warning) << "Failed to parse 409 conflict body."; } + // Capture the user id up front so the force-push closure does not have to touch m_agent. + std::string conflict_user_id = m_agent ? m_agent->get_user_id() : std::string(); auto* plater = wxGetApp().plater(); if (plater != nullptr && wxGetApp().imgui()->display_initialized()) { std::string text; - if (conflict_code == -1) { + + switch (conflict_code) { + case -1: text = _u8L("Cloud sync conflict: this preset has a newer version in OrcaCloud.\n" "Pull downloads the cloud copy. Force push overwrites it with your local preset."); - } else { + break; + case -2: text = _u8L("Cloud sync conflict: a preset with this name already exists in OrcaCloud.\n" "Pull downloads the cloud copy. Force push overwrites it with your local preset."); - } + break; + case -3: + text = _u8L("Cloud sync conflict: a preset with the same name was previously deleted from the cloud.\n" + "Delete will delete your local preset. Force push overwrites it with your local preset."); + break; + default: + text = _u8L("Cloud sync conflict: there was an unexpected or unidentified preset conflict.\n" + "Pull downloads the cloud copy. Force push overwrites it with your local preset."); + break; + }; + plater->get_notification_manager()->push_orca_sync_conflict_notification( - text, + text, conflict_code, [this](wxEvtHandler*) { // Runs on the GUI thread (on_http_error is a queued wx event); restart_sync_user_preset() // already joins the old sync thread off the UI thread, so no extra thread is needed here. @@ -4971,7 +4991,7 @@ void GUI_App::on_http_error(wxCommandEvent &evt) restart_sync_user_preset(); return true; }, - [this, conflict_setting_id](wxEvtHandler*) { + [this, conflict_setting_id, conflict_preset_name, conflict_user_id](wxEvtHandler*) { if (mainframe == nullptr) return false; MessageDialog @@ -4981,7 +5001,13 @@ void GUI_App::on_http_error(wxCommandEvent &evt) if (dlg.ShowModal() != wxID_YES) return false; - force_push_conflicting_preset(conflict_setting_id); + std::string setting_id = conflict_setting_id; + if (setting_id.empty()) { + setting_id = OrcaCloudServiceAgent::generate_uuid_for_setting_id(conflict_preset_name, conflict_user_id); + BOOST_LOG_TRIVIAL(info) << "conflict setting id empty, generated one: " << setting_id; + } + + force_push_conflicting_preset(setting_id); return true; }); } @@ -7055,15 +7081,26 @@ void GUI_App::force_push_conflicting_preset(const std::string& setting_id) m_pending_conflict_setting_ids.push_back(setting_id); } + const std::string user_id = m_agent ? m_agent->get_user_id() : std::string(); + // The 409 left this preset on "hold", which get_user_presets() skips. Restore it to // "update" so the next push-sync re-includes it and consumes the queued force flag. // (We must NOT pull from the cloud here as the Pull path does — that would overwrite // the local changes the user is trying to force-push.) + // For a -3 tombstone on a newly created preset the on-disk setting_id is EMPTY (it only + // gets assigned after a successful first push), so derive it on the fly from the preset + // name and stamp it onto the preset — otherwise sync_with_lock's `id == preset.setting_id` + // check never fires and the force-push silently no-ops. PresetCollection* collections[] = {&preset_bundle->prints, &preset_bundle->filaments, &preset_bundle->printers}; for (PresetCollection* coll : collections) { for (const Preset& preset : coll->get_presets()) { - if (preset.setting_id == setting_id && preset.sync_info == "hold") { - coll->set_sync_info_and_save(preset.name, preset.setting_id, "update", 0); + if (preset.sync_info != "hold") + continue; + const std::string preset_id = preset.setting_id.empty() + ? OrcaCloudServiceAgent::generate_uuid_for_setting_id(preset.name, user_id) + : preset.setting_id; + if (preset_id == setting_id) { + coll->set_sync_info_and_save(preset.name, setting_id, "update", 0); break; } } diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 5079dfecf0..2e8d22ebee 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -2416,7 +2416,7 @@ void NotificationManager::OrcaSyncConflictNotification::render_text(ImGuiWrapper } const float action_y = starting_y + m_endlines.size() * shift_y; - const std::string pull_text = _u8L("Pull"); + const std::string pull_text = conflict_code == -3 ? _u8L("Delete") : _u8L("Pull"); render_hyperlink_action(imgui, x_offset, action_y, pull_text, "##orca_sync_pull", [this] { if (m_pull_callback && m_pull_callback(m_evt_handler)) close(); }); if (m_force_push_callback) { @@ -2437,13 +2437,14 @@ void NotificationManager::push_shared_profiles_notification(const std::string& e } void NotificationManager::push_orca_sync_conflict_notification(const std::string& text, + int conflict_code, std::function pull_callback, std::function force_push_callback) { close_notification_of_type(NotificationType::OrcaSyncConflict); NotificationData data{ NotificationType::OrcaSyncConflict, NotificationLevel::WarningNotificationLevel, 0, text }; push_notification_data(std::make_unique( - data, m_id_provider, m_evt_handler, std::move(pull_callback), std::move(force_push_callback)), 0); + data, m_id_provider, m_evt_handler, std::move(pull_callback), std::move(force_push_callback), conflict_code), 0); } void NotificationManager::push_download_URL_progress_notification(size_t id, const std::string& text, std::function user_action_callback) diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 006de5cff6..dd17f19e42 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -279,6 +279,7 @@ public: // Shared profiles available for selected printer void push_shared_profiles_notification(const std::string& explore_url); void push_orca_sync_conflict_notification(const std::string& text, + int conflict_code, std::function pull_callback, std::function force_push_callback); @@ -905,10 +906,12 @@ private: public: OrcaSyncConflictNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, std::function pull_callback, - std::function force_push_callback) + std::function force_push_callback, + int conflict_code) : PopNotification(n, id_provider, evt_handler) , m_pull_callback(std::move(pull_callback)) , m_force_push_callback(std::move(force_push_callback)) + , conflict_code(conflict_code) { m_multiline = true; } @@ -920,6 +923,7 @@ private: std::function m_pull_callback; std::function m_force_push_callback; + int conflict_code; }; class SlicingProgressNotification; diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp index 02804d1598..61eb388b92 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp @@ -101,24 +101,6 @@ std::string resolve_display_name( return username; } -std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = "") -{ - if (name.empty()) { - return ""; - } - - // Mix user_id into the hashed input so two different users generating a setting_id - // for an identically-named preset get distinct UUIDs. Without this, the cloud's ID - // space collides across accounts and the second user's create gets HTTP 409 with - // server_profile=null on every sync (the foreign owner's record is not exposed). - static const boost::uuids::uuid orca_namespace = - boost::uuids::string_generator()("f47ac10b-58cc-4372-a567-0e02b2c3d479"); - - boost::uuids::name_generator_sha1 gen(orca_namespace); - boost::uuids::uuid id = user_id.empty() ? gen(name) : gen(user_id + "/" + name); - return boost::uuids::to_string(id); -} - std::string base64url_encode(const std::vector& data) { std::string out; @@ -412,6 +394,24 @@ OrcaCloudServiceAgent::~OrcaCloudServiceAgent() } } +std::string OrcaCloudServiceAgent::generate_uuid_for_setting_id(const std::string& name, const std::string& user_id) +{ + if (name.empty()) { + return ""; + } + + // Mix user_id into the hashed input so two different users generating a setting_id + // for an identically-named preset get distinct UUIDs. Without this, the cloud's ID + // space collides across accounts and the second user's create gets HTTP 409 with + // server_profile=null on every sync (the foreign owner's record is not exposed). + static const boost::uuids::uuid orca_namespace = + boost::uuids::string_generator()("f47ac10b-58cc-4372-a567-0e02b2c3d479"); + + boost::uuids::name_generator_sha1 gen(orca_namespace); + boost::uuids::uuid id = user_id.empty() ? gen(name) : gen(user_id + "/" + name); + return boost::uuids::to_string(id); +} + void OrcaCloudServiceAgent::configure_urls(AppConfig* app_config) { if (!app_config) return; @@ -1250,8 +1250,10 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id, if (http_code == 409) { // Conflict - parse server version + nlohmann::json err_body; try { auto json = nlohmann::json::parse(response); + err_body = json; if (json.is_null()) { result.server_deleted = true; } else { @@ -1261,6 +1263,13 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id, result.server_version.updated_time = profile_data.value(ORCA_JSON_KEY_UPDATE_TIME, 0); } } catch (...) {} + // Surface the conflict via the http-error callback with the local preset name injected. + // The raw server body omits the name for tombstone (-3) conflicts (server_profile is null), + // but the GUI needs it to regenerate the deterministic setting_id for a force push. + if (!err_body.is_object()) + err_body = nlohmann::json::object(); + err_body["name"] = name; + invoke_http_error_callback(409, err_body.dump()); result.error_message = response; return result; } @@ -1937,7 +1946,10 @@ int OrcaCloudServiceAgent::http_post(const std::string& path, const std::string& if (response_body) *response_body = res.body; if (http_code) *http_code = res.status; - if (!suppress && (!res.success || res.status >= 400)) { + // 409 is a push-only domain conflict; sync_push re-fires the error callback with the + // local preset name injected (the raw server body omits it for tombstone conflicts), + // so skip the generic nameless auto-fire here to avoid a duplicate, nameless event. + if (!suppress && (!res.success || res.status >= 400) && res.status != 409) { invoke_http_error_callback(res.status, res.body); } diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp index 6852992198..8820425f7f 100644 --- a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp +++ b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp @@ -300,6 +300,8 @@ public: bool set_user_session(const nlohmann::json& session_json, bool notify_login = true); void clear_session(); + static std::string generate_uuid_for_setting_id(const std::string& name, const std::string& user_id = ""); + private: // Sync protocol helpers int sync_pull(