mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
A reference is a reference whatever feature holds it
Port of snaporca 1bb9825db0. Four defects from an independent 20-agent audit, each verified in the code first; two further findings from the same report were verified OUT and are not in this commit. remove_feature()/move_feature() remapped Extrude::sketch_ref and a Mate's two connectors and nothing else, leaving seven of the nine index-bearing fields — sweep_path_ref, loft_profile_refs[], pattern_curve_sketch, rib_sketch_ref, and sketch_ref on Revolve, Sweep, Rib and the Surface* family — pointing at whatever slid into the slot. Quiet by construction: the shifted index still names a real feature, recompute() succeeds, the solid is built from the wrong profile. The comment above the loop already required "EVERY field holding a feature index"; the code under it handled two, because a type switch is only correct on the day it is written. for_each_feature_ref() visits the FIELDS instead, so a feature type added later is covered the moment it reuses one. plane_base and axis_plane_a/b are excluded on purpose and documented at the helper — they encode an ordinal into the datum-plane list, not an index into features[], and are filed separately. The delete cascade got the same field-based treatment. The regression test was run against the pre-fix code to prove it bites: all three sections fail there, and move_feature returns TRUE while leaving sketch_ref == 1 where it must be 0 — success with the wrong answer, which is what makes this class expensive. apply_constraint, commit_entity_constraints and delete_constraint mutated the recipe with no checkpoint() and no sync_recipe_to_model(), alone among seventeen mutation sites in that file: Ctrl+Z reached past the constraint edit and discarded unrelated work, and saving persisted the pre-constraint blob. A rejected constraint now calls abandon_checkpoint() rather than leaving an undo step that does nothing. MCP: params["generation"].get<uint64_t>() sat outside the try inside a bare CallAfter lambda, so one malformed string terminated the process through the wx event loop; it is type-checked now and the lambda lets nothing escape. The socket bound with no mode of its own in a world-writable directory — umask around bind() plus chmod, and it refuses to listen rather than listen wide. The reply write is no longer a bare write(), which could SIGPIPE the app when a client hung up. Kernel suite on this fork: 190 cases / 2562 assertions, green. GUI target compiles. The full ladder gate ran on snaporca (ALL LADDERS HELD — gestures 98/98, offer 108/108, corpus and corpus-scale green) and fork-check parity holds at 17 identical / 8 diverging as expected, which is what makes that gate transferable here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
This commit is contained in:
co-authored by
Claude Opus 5
parent
6e7f6429fa
commit
3fd5c3353a
@@ -7850,16 +7850,25 @@ void DesignPanel::commit_entity_constraints(const std::vector<SketchEntityConstr
|
||||
// (Symmetric on two lines) must solve together, so push all then resize back.
|
||||
const std::vector<SketchEntity> saved = feat.entities;
|
||||
const size_t before = feat.entity_constraints.size();
|
||||
// Undo boundary: adding a constraint. Without it Ctrl+Z reached PAST this edit to the
|
||||
// previous boundary and threw away whatever happened in between — a constraint was the
|
||||
// one document mutation the user could not take back on its own.
|
||||
m_doc.checkpoint();
|
||||
for (const auto& d : defs) feat.entity_constraints.push_back(d);
|
||||
if (!m_doc.solve_sketch_feature(m_constrain_feat)) {
|
||||
feat.entity_constraints.resize(before);
|
||||
feat.entities = saved;
|
||||
m_doc.abandon_checkpoint(); // fully restored above: nothing happened, so nothing to undo
|
||||
m_status->SetForegroundColour(wxColour(235, 110, 110));
|
||||
set_status(_L("Constraint rejected (over-constrained)"));
|
||||
m_status->Refresh();
|
||||
return;
|
||||
}
|
||||
m_doc.recompute();
|
||||
// The constraint lives in the recipe, so the save path has to be told the recipe moved;
|
||||
// otherwise saving after a constraint edit wrote the blob from before it.
|
||||
sync_recipe_to_model();
|
||||
update_undo_redo_buttons();
|
||||
m_viewport->update_constrain_entities(m_doc.features[m_constrain_feat].entities);
|
||||
if (!m_doc.display_mesh.its.indices.empty())
|
||||
feed_bodies();
|
||||
@@ -8029,11 +8038,14 @@ void DesignPanel::delete_constraint(int idx)
|
||||
CadFeature& feat = m_doc.features[m_constrain_feat];
|
||||
if (idx < 0 || idx >= int(feat.entity_constraints.size()))
|
||||
return;
|
||||
m_doc.checkpoint(); // undo boundary: deleting a constraint
|
||||
feat.entity_constraints.erase(feat.entity_constraints.begin() + idx);
|
||||
// Re-solve the remaining system (deleting a constraint can only free DoF, so it
|
||||
// cannot fail for over-constraint; ignore the bool and refresh either way).
|
||||
m_doc.solve_sketch_feature(m_constrain_feat);
|
||||
m_doc.recompute();
|
||||
sync_recipe_to_model(); // the removal is part of the recipe
|
||||
update_undo_redo_buttons();
|
||||
m_viewport->set_constraint_highlight({});
|
||||
m_viewport->update_constrain_entities(m_doc.features[m_constrain_feat].entities);
|
||||
if (!m_doc.display_mesh.its.indices.empty())
|
||||
@@ -8867,16 +8879,20 @@ void DesignPanel::apply_constraint(SketchConstraintType type)
|
||||
// solve_sketch_feature rewrites profile.points even on failure, so snapshot
|
||||
// the geometry to roll back a rejected constraint cleanly.
|
||||
const std::vector<Vec2d> saved_pts = feat.profile.points;
|
||||
m_doc.checkpoint(); // undo boundary: adding a constraint (profile sketches)
|
||||
feat.constraints.push_back(SketchConstraintDef{type, a, b, -1, -1, 0.0});
|
||||
if (!m_doc.solve_sketch_feature(m_constrain_feat)) {
|
||||
feat.constraints.pop_back(); // reject the non-converging addition
|
||||
feat.profile.points = saved_pts; // and restore the pre-solve geometry
|
||||
m_doc.abandon_checkpoint(); // restored: no state change, so no undo step
|
||||
m_status->SetForegroundColour(wxColour(235, 110, 110));
|
||||
set_status(_L("Constraint rejected (over-constrained)"));
|
||||
m_status->Refresh();
|
||||
return;
|
||||
}
|
||||
m_doc.recompute();
|
||||
sync_recipe_to_model();
|
||||
update_undo_redo_buttons();
|
||||
m_viewport->update_constrain_profile(m_doc.features[m_constrain_feat].profile.points);
|
||||
if (!m_doc.display_mesh.its.indices.empty())
|
||||
feed_bodies();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/stat.h> // umask/chmod: the socket's file mode IS its access control
|
||||
#include <unistd.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -2008,7 +2009,14 @@ std::string handle_on_main(const std::string& method, const json& params, const
|
||||
// keeps every existing script working exactly as before, and supplying it is what buys the
|
||||
// guarantee. One check at the dispatcher rather than one per handler, so a method added
|
||||
// later cannot forget it.
|
||||
//
|
||||
// The type check is not decoration: this runs OUTSIDE the try below, and a bare
|
||||
// get<uint64_t>() on `"generation": "x"` throws nlohmann::type_error straight out of
|
||||
// the CallAfter lambda that invoked us — through a wx event loop, which does not catch,
|
||||
// so the whole GUI went down on one malformed line. Refuse it as a parameter error.
|
||||
if (params.is_object() && params.contains("generation")) {
|
||||
if (!params["generation"].is_number_unsigned())
|
||||
return rpc_error(id, -32602, "generation must be an unsigned integer");
|
||||
const uint64_t want = params["generation"].get<uint64_t>();
|
||||
const uint64_t have = panel->mcp_doc().topo_generation;
|
||||
if (want != have)
|
||||
@@ -2099,8 +2107,18 @@ std::string dispatch_request(const std::string& line)
|
||||
|
||||
auto prom = std::make_shared<std::promise<std::string>>();
|
||||
auto fut = prom->get_future();
|
||||
// Nothing may escape this lambda. It is invoked by the wx event loop, which has no
|
||||
// handler of its own, so an escaping exception is std::terminate — the socket would
|
||||
// become a way for any client to kill the application. handle_on_main() catches what
|
||||
// it knows about; this catches what it does not, and still answers the caller.
|
||||
wxGetApp().CallAfter([prom, method, params, id]() {
|
||||
prom->set_value(handle_on_main(method, params, id));
|
||||
try {
|
||||
prom->set_value(handle_on_main(method, params, id));
|
||||
} catch (const std::exception& ex) {
|
||||
prom->set_value(rpc_error(id, -32000, std::string("internal error: ") + ex.what()));
|
||||
} catch (...) {
|
||||
prom->set_value(rpc_error(id, -32000, "internal error: unknown exception"));
|
||||
}
|
||||
});
|
||||
if (fut.wait_for(std::chrono::seconds(15)) != std::future_status::ready)
|
||||
return rpc_error(id, -32000, "main-thread timeout");
|
||||
@@ -2123,7 +2141,14 @@ void serve_client(int cfd)
|
||||
if (line.empty()) continue;
|
||||
std::string reply = dispatch_request(line);
|
||||
reply.push_back('\n');
|
||||
if (::write(cfd, reply.data(), reply.size()) < 0) return;
|
||||
// Never a bare write(): a client that hangs up between its request and our
|
||||
// reply raises SIGPIPE, whose default action kills the process — so closing
|
||||
// a socket mid-call would take the GUI with it.
|
||||
#ifdef MSG_NOSIGNAL
|
||||
if (::send(cfd, reply.data(), reply.size(), MSG_NOSIGNAL) < 0) return;
|
||||
#else
|
||||
if (::write(cfd, reply.data(), reply.size()) < 0) return; // SO_NOSIGPIPE set at accept
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2137,16 +2162,33 @@ void server_thread(std::string sock_path)
|
||||
sockaddr_un addr{};
|
||||
addr.sun_family = AF_UNIX;
|
||||
std::strncpy(addr.sun_path, sock_path.c_str(), sizeof(addr.sun_path) - 1);
|
||||
if (::bind(sfd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
|
||||
// The socket is the full CAD command surface, including import_step on absolute paths.
|
||||
// It lands in a world-writable directory by default (/tmp), so its access control is
|
||||
// its file mode and nothing else — leaving that to the ambient umask means any local
|
||||
// process may drive the modeller. umask around bind() makes it 0600 with no window in
|
||||
// which a wider mode exists; the chmod afterwards covers platforms that do not apply
|
||||
// umask to sockets.
|
||||
const mode_t old_umask = ::umask(0177);
|
||||
const int bind_rc = ::bind(sfd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
|
||||
::umask(old_umask);
|
||||
if (bind_rc < 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MCP: bind() failed on " << sock_path;
|
||||
::close(sfd); return;
|
||||
}
|
||||
if (::chmod(sock_path.c_str(), S_IRUSR | S_IWUSR) < 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << "MCP: cannot restrict " << sock_path << " to the owner; refusing to listen";
|
||||
::close(sfd); ::unlink(sock_path.c_str()); return;
|
||||
}
|
||||
if (::listen(sfd, 1) < 0) { BOOST_LOG_TRIVIAL(error) << "MCP: listen() failed"; ::close(sfd); return; }
|
||||
BOOST_LOG_TRIVIAL(info) << "MCP control listening on " << sock_path;
|
||||
|
||||
for (;;) {
|
||||
int cfd = ::accept(sfd, nullptr, nullptr);
|
||||
if (cfd < 0) continue;
|
||||
#if !defined(MSG_NOSIGNAL) && defined(SO_NOSIGPIPE)
|
||||
const int on = 1; // macOS/BSD equivalent of MSG_NOSIGNAL
|
||||
::setsockopt(cfd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on));
|
||||
#endif
|
||||
serve_client(cfd);
|
||||
::close(cfd);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user