fix(drift-trio): phase E — serialized fiber queue (#10a) + fiber-busy probes

runOnFiber now runs bodies strictly one-at-a-time through a park-safe FIFO
(collab_common.h): the per-body fire-and-forget coroutine interleaved under
load — an asyncify park inside commit.Push let the event loop start the next
body, so a local commit and a remote apply ran interleaved on shared state
(s_applyingRemote is one global), silently losing applies on the actively-
editing receiver (fuzz finding #10a; B now fuzzes clean; 39-test suite green).

kicadCollabFiberBusy embind probe (merged + standalone registrations): a
bare-embind-stack scratch save during a parked fiber mis-dispatches (table
index OOB) — trio.ts modelText/drift and production drift-detect now defer
while fiber work is in flight (#10b hardening; the trap's root cause is still
open and needs a symbolized stack — fuzz stays fixme'd).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
This commit is contained in:
Gergő Törcsvári 2026-07-21 12:22:09 +02:00
commit 3fc90e8fe2
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
6 changed files with 91 additions and 9 deletions

View file

@ -11,6 +11,7 @@
#ifdef __EMSCRIPTEN__
#include <deque>
#include <emscripten.h>
#include <functional>
#include <string>
@ -34,17 +35,60 @@ inline std::string toUtf8( const wxString& s ) { return std::string( s.utf8_str(
* the s-expr formatters must therefore run through this. CallAfter queues
* onto the app's pending-event list (drained every frame by the wasm main
* loop, src/wasm/evtloop.cpp); COROUTINE::Call moves the body to the fiber.
*
* SERIALIZED (drift-trio finding #10, standalone-hardening 0008 §10): bodies
* run strictly one-at-a-time through a FIFO. The previous per-body
* fire-and-forget coroutine interleaved under load: when a body PARKED
* (asyncify suspension inside commit.Push — connectivity/GAL work), the main
* loop kept draining pending events and started the NEXT body — a local
* commit and a remote apply then ran interleaved on shared commit/listener
* state (s_applyingRemote is a single global), silently losing applies on the
* actively-editing receiver and, in the worst case, corrupting memory (fuzz
* S10: wasm OOB on an observer). The busy flag is park-safe: an asyncify
* suspension suspends the whole drain loop with the body and rewinds it
* transparently, while any other drain invocation no-ops on the flag; the
* suspended drain's own while-loop picks up whatever queued meanwhile.
*/
inline void runOnFiber( wxEvtHandler* aHandler, std::function<void()> aBody )
inline std::deque<std::function<void()>>& fiberQueue()
{
aHandler->CallAfter( [aBody]() {
COROUTINE<int, int> cor( [&aBody]( int ) -> int
static std::deque<std::function<void()>> q;
return q;
}
inline bool& fiberBusy()
{
static bool busy = false;
return busy;
}
inline void drainFibers()
{
if( fiberBusy() )
return; // the running drain's while-loop covers the rest
auto& q = fiberQueue();
while( !q.empty() )
{
fiberBusy() = true;
std::function<void()> body = std::move( q.front() );
q.pop_front();
COROUTINE<int, int> cor( [&body]( int ) -> int
{
aBody();
body();
return 0;
} );
cor.Call( 0 );
} );
fiberBusy() = false;
}
}
inline void runOnFiber( wxEvtHandler* aHandler, std::function<void()> aBody )
{
fiberQueue().push_back( std::move( aBody ) );
aHandler->CallAfter( []() { drainFibers(); } );
}
// ── C++ → JS wire emitters (no-ops without a JS listener) ───────────────────

View file

@ -1978,6 +1978,11 @@ void kicadSaveSchematic( std::string path )
}
static bool kicadCollabFiberBusyProbe()
{
return pcbjam_collab::fiberBusy() || !pcbjam_collab::fiberQueue().empty();
}
EMSCRIPTEN_BINDINGS(eeschema) {
// Programmatic save of the in-memory schematic (round-trip tests, README §A).
function("kicadSaveSchematic", &kicadSaveSchematic);
@ -1998,6 +2003,7 @@ EMSCRIPTEN_BINDINGS(eeschema) {
// registered once by kicad_editor_embind.cpp, dispatching on the active frame.
// Programmatic file open (preferred over UI automation from the web app).
function("kicadOpenFile", &kicadOpenFile);
function("kicadCollabFiberBusy", &kicadCollabFiberBusyProbe);
// Read-only viewer lock (read-only-viewer).
function("kicadSetReadOnly", &kicadSetReadOnly);
// Yjs collaborative bridge entry points (same contract as pl_editor).

View file

@ -451,7 +451,16 @@ static bool collabTestClearSelection()
}
static bool kicadCollabFiberBusyProbe()
{
return pcbjam_collab::fiberBusy() || !pcbjam_collab::fiberQueue().empty();
}
EMSCRIPTEN_BINDINGS(kicad_editor) {
// Fiber-queue idle probe (drift-trio finding #10b): a bare-embind-stack
// save during a parked apply fiber mis-dispatches (table index OOB) — the
// JS side must defer scratch saves while collab fiber work is in flight.
function("kicadCollabFiberBusy", &kicadCollabFiberBusyProbe);
// Programmatic file open (preferred over UI automation from the web app).
function("kicadOpenFile", &kicadOpenFile);

View file

@ -2283,6 +2283,11 @@ std::string Pad_GetPinFunction(PAD* pad) {
return pad->GetPinFunction().ToStdString();
}
static bool kicadCollabFiberBusyProbe()
{
return pcbjam_collab::fiberBusy() || !pcbjam_collab::fiberQueue().empty();
}
EMSCRIPTEN_BINDINGS(pcbnew) {
// Register vector types for iteration
register_vector<FOOTPRINT*>("FootprintVector");
@ -2321,6 +2326,7 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
// registered once by kicad_editor_embind.cpp, dispatching on the active frame.
// Programmatic file open (preferred over UI automation from the web app).
function("kicadOpenFile", &kicadOpenFile);
function("kicadCollabFiberBusy", &kicadCollabFiberBusyProbe);
// Read-only viewer lock (read-only-viewer).
function("kicadSetReadOnly", &kicadSetReadOnly);
// Yjs collaborative bridge entry points (same contract as pl_editor / eeschema).