pcbjam/scripts/common/shims/handlesleep.js
Viktor Vaczi 14ca16cbd3 test(asyncify): red-green race harness + ablation flags, unwind-catch shim, spec tightening, decisions docs
The asyncify single-slot work, executed red-green (full ledger:
docs/features/asyncify-arbiter/redgreen.md; decisions record:
docs/features/async/07-decisions-and-outcome.md):

- tests/apps/standalone/asyncify-races/ + tests/asyncify/ + dedicated
  playwright config: 8 scenarios reproducing the KiCad asyncify failure
  family with the kicad-faithful startup topology (pre-park fiber swap →
  park throw through the live trampoline). Built in 3 variants; the
  SHIM_DISABLE_TRAMPOLINE_HEAL / SHIM_DISABLE_HANDLESLEEP ablation builds
  keep the historical hang and index-out-of-bounds crash reproducible
  forever (mutation-style pins for the existing shims).
- scripts/common/shims/handlesleep.js: catch the "unwind" park sentinel
  in the wakeUp path — when main's last pre-park suspension was a sleep,
  the main-loop park throw escaped through that sleep's promise reaction
  as an uncaught rejection (the calculator/gerbview console errors).
- scripts/common/inject-dyncall-shims.sh: SHIM_DISABLE_* ablation knobs.
- Spec tightening (the acceptance bar): 'uncaught exception: unwind'
  tolerance DELETED from pcbnew/eeschema specs; load-pcb gained a hard
  clean-console gate over 5 asyncify corruption signatures.
- wxwidgets pointer bump: modal LIFO resolvers, pump resolve-on-error,
  sync clipboard IsSupported (014f67e6c1).

Final state: asyncify suite 7/7, wx e2e 291/292 (1 skip), KiCad e2e 40
passed / 2 skipped with ZERO corruption signatures in any log across all
six apps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:02:59 +02:00

80 lines
3.3 KiB
JavaScript

// === Nested-Asyncify handleSleep currData save/restore (Emscripten #9153) ===
//
// Asyncify.currData is a single-slot global. When a fiber swap runs inside an
// EM_ASYNC_JS Promise await (e.g., wxDialog::ShowModal via startModal), the
// fiber swap overwrites currData with the fiber's asyncify_data, losing the
// sleep's own buffer. On Promise resolution, handleSleep's doRewind then uses
// the wrong buffer and crashes with "index out of bounds" or "unreachable".
//
// Workaround: intercept Asyncify.allocateData to record which pointer belongs to
// the active handleSleep; restore it to Asyncify.currData inside the wakeUp
// callback before handleSleep proceeds to _asyncify_start_rewind + doRewind.
if (typeof Asyncify !== "undefined") {
if (typeof Asyncify.handleSleep === "function"
&& typeof Asyncify.allocateData === "function"
&& !Asyncify.__nestedHandleSleepInstalled) {
// Stack of handleSleep contexts awaiting their allocateData association.
Asyncify.__pendingSleepContexts = [];
var __originalAllocateData = Asyncify.allocateData.bind(Asyncify);
Asyncify.allocateData = function() {
var ptr = __originalAllocateData();
// Associate with the innermost pending handleSleep not yet linked.
for (var i = Asyncify.__pendingSleepContexts.length - 1; i >= 0; --i) {
var ctx = Asyncify.__pendingSleepContexts[i];
if (!ctx.capturedData) {
ctx.capturedData = ptr;
break;
}
}
return ptr;
};
var __originalHandleSleep = Asyncify.handleSleep.bind(Asyncify);
Asyncify.handleSleep = function(startAsync) {
var sleepCtx = { capturedData: null, cleanedUp: false };
Asyncify.__pendingSleepContexts.push(sleepCtx);
var cleanup = function() {
if (sleepCtx.cleanedUp) return;
sleepCtx.cleanedUp = true;
var idx = Asyncify.__pendingSleepContexts.indexOf(sleepCtx);
if (idx !== -1) Asyncify.__pendingSleepContexts.splice(idx, 1);
};
try {
return __originalHandleSleep(function(wakeUp) {
return startAsync(function(result) {
// wakeUp runs from pure JS on Promise resolution. Fiber swaps during
// the await may have overwritten Asyncify.currData. Restore OUR buffer
// so handleSleep's _asyncify_start_rewind and doRewind use it.
if (sleepCtx.capturedData) {
Asyncify.currData = sleepCtx.capturedData;
}
cleanup();
try {
return wakeUp(result);
} catch (e) {
// emscripten_set_main_loop(...,1) parks main() by throwing the
// "unwind" sentinel. When main's LAST pre-park suspension was a
// sleep, main is resumed from THIS wakeUp, so the sentinel
// propagates here instead of into callMain's catch — surfacing as
// an uncaught "unwind" promise rejection. Swallow it exactly like
// callMain/handleException do on the direct path.
if (e === "unwind") {
return;
}
throw e;
}
});
});
} catch (e) {
cleanup();
throw e;
}
};
Asyncify.__nestedHandleSleepInstalled = true;
}
}
// === End nested-Asyncify handleSleep fix ===