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:
parent
a5751d8542
commit
3fc90e8fe2
6 changed files with 91 additions and 9 deletions
|
|
@ -322,11 +322,19 @@ export async function closeTrio(trio: Trio): Promise<void> {
|
|||
|
||||
// ── Per-tab probes ───────────────────────────────────────────────────────────
|
||||
|
||||
/** Silent save-to-MEMFS + read back — no onSave side effects. */
|
||||
/** Silent save-to-MEMFS + read back — no onSave side effects. Defers while
|
||||
* collab fiber work is in flight: a bare-embind-stack save during a parked
|
||||
* apply mis-dispatches (finding #10b) — the wait is JS-side, so it is safe. */
|
||||
export function modelText(page: Page, cfg: ToolCfg): Promise<string> {
|
||||
return page.evaluate(
|
||||
({ saveFn, ext }) => {
|
||||
const w = window as unknown as { FS: FSApi; Module: Mod };
|
||||
async ({ saveFn, ext }) => {
|
||||
const w = window as unknown as {
|
||||
FS: FSApi;
|
||||
Module: Mod & { kicadCollabFiberBusy?: () => boolean };
|
||||
};
|
||||
for (let i = 0; i < 200 && w.Module.kicadCollabFiberBusy?.(); i++) {
|
||||
await new Promise((r) => setTimeout(r, 25));
|
||||
}
|
||||
const out = `/home/kicad/documents/_dump.${ext}`;
|
||||
(w.Module[saveFn] as (p: string) => unknown)(out);
|
||||
return w.FS.readFile(out, { encoding: "utf8" });
|
||||
|
|
@ -360,10 +368,14 @@ export interface DriftSummary {
|
|||
/** Item-level drift summary via the production comparator (browser-entry-v2). */
|
||||
export function drift(page: Page, cfg: ToolCfg): Promise<DriftSummary | null> {
|
||||
return page.evaluate(
|
||||
({ saveFn, ext }) => {
|
||||
async ({ saveFn, ext }) => {
|
||||
const w = window as unknown as {
|
||||
KicadCollabV2: { driftReport(f: string, p: string): DriftSummary | null };
|
||||
Module: { kicadCollabFiberBusy?: () => boolean };
|
||||
};
|
||||
for (let i = 0; i < 200 && w.Module.kicadCollabFiberBusy?.(); i++) {
|
||||
await new Promise((r) => setTimeout(r, 25));
|
||||
}
|
||||
return w.KicadCollabV2.driftReport(saveFn, `/home/kicad/documents/_drift.${ext}`);
|
||||
},
|
||||
{ saveFn: cfg.saveFn, ext: cfg.ext },
|
||||
|
|
|
|||
|
|
@ -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) ───────────────────
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -125,6 +125,11 @@ export function startDriftDetection(opts: DriftDetectOptions): DriftDetector {
|
|||
// unchanged since the last report, or the session cap is spent). Being fully
|
||||
// synchronous is what lets the session-end check finish during page unload.
|
||||
function computeDrift(): DriftReportBody | null {
|
||||
// Defer while collab fiber work is in flight (0008 finding #10b): a save
|
||||
// on the bare embind stack during a parked apply fiber mis-dispatches
|
||||
// (table index OOB). The next Y-update trigger retries.
|
||||
const busy = (opts.mod as { kicadCollabFiberBusy?: () => boolean }).kicadCollabFiberBusy;
|
||||
if (busy?.()) return null;
|
||||
save(scratchPath);
|
||||
let text: unknown;
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue