fix(web): eeschema schematic open path — Asyncify/dynCall fixes + bump submodules

- inject-dyncall-shims.sh: add a dynCallLegacy -> wasmExports fallback so embind's
  generic dynCall path finds the DYNCALLS=1 trampolines. Without it, an Asyncify
  unwind/rewind through an embind call (kicadOpenFile -> OpenProjectFiles) died
  with "f is not a function".
- open-flow.ts: kicadOpenFile runs OpenProjectFiles under Asyncify, so its sync
  return is a falsy placeholder. Invoke it and poll the frame title for the load
  instead of trusting the return value, and never fall back to UI automation
  while the hook is in flight (it would re-enter the suspended Asyncify call).
- .gitignore: ignore .playwright-mcp scratch.
- bump kicad + wxwidgets submodules to the wasm schematic-open fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergő Törcsvári 2026-06-01 22:30:04 +02:00
commit ddd959fbc2
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
5 changed files with 59 additions and 12 deletions

2
.gitignore vendored
View file

@ -60,3 +60,5 @@ wxwidgets-clean/
output/
*.d
/tests/.test-port-coroutine
.playwright-mcp

2
kicad

@ -1 +1 @@
Subproject commit 211ab47df18abe03af3c8c1c8cfbbe5a42520129
Subproject commit d701b2365aeb327b9737d97b72aafa1da4d07dc2

View file

@ -136,6 +136,22 @@ else
fi
fi
# --- 3b. embind dynCall fallback (dynCallLegacy -> wasmExports) ----------------
# embind's generic caller (getDynCaller) routes through dynCallLegacy, which only
# reads Module["dynCall_<sig>"]. But the DYNCALLS=1 trampolines are wasm EXPORTS,
# not Module properties, so that lookup is undefined and an Asyncify unwind/rewind
# through an embind call (e.g. kicadOpenFile -> OpenProjectFiles) dies with
# "f is not a function" in Asyncify.doRewind. Add a wasmExports fallback so the
# instrumented trampoline is found and rewind survives.
if grep -q 'embind dynCall fallback installed' "$JS_FILE"; then
echo "dynCallLegacy fallback already present - skipping"
elif grep -qF ' var f = Module["dynCall_" + sig];' "$JS_FILE"; then
perl -0pi -e 's/(\Q var f = Module["dynCall_" + sig];\E)/$1\n \/\/ embind dynCall fallback installed: DYNCALLS=1 trampolines live on wasmExports, not Module.\n if (!f && typeof wasmExports !== "undefined") f = wasmExports["dynCall_" + sig];/' "$JS_FILE"
echo "Injected dynCallLegacy wasmExports fallback"
else
echo "Warning: dynCallLegacy pattern not found - skipping embind dynCall fallback"
fi
# --- 4. Optional diagnostics (logging only) -----------------------------------
if [ "$SHIM_DIAGNOSTICS" = "1" ]; then
if grep -q 'DIAG] Asyncify/fiber/modal diagnostics installed' "$JS_FILE"; then

View file

@ -81,19 +81,34 @@ function pressKey(win: ToolWindow, key: string): void {
el.dispatchEvent(new KE("keyup", init));
}
function tryProgrammaticOpen(
/** True if the build exposes the programmatic open hook. */
function hasProgrammaticHook(win: ToolWindow): boolean {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mod = win.Module as any;
return !!mod && typeof mod.kicadOpenFile === "function";
}
/**
* Invoke the programmatic hook. NOTE: kicadOpenFile runs OpenProjectFiles under
* Asyncify, so the call SUSPENDS and unwinds back to JS before the load finishes
* — its synchronous return is a falsy placeholder, not the real bool. So we fire
* it and ignore the return; the caller polls for the loaded schematic instead.
*/
function invokeProgrammaticOpen(
win: ToolWindow,
absPath: string,
log: (m: string) => void,
): boolean {
): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mod = win.Module as any;
if (mod && typeof mod.kicadOpenFile === "function") {
const ok = mod.kicadOpenFile(absPath) === true;
log(`[open] Module.kicadOpenFile(${absPath}) -> ${ok}`);
return ok; // false → caller falls back to UI automation
}
return false;
mod.kicadOpenFile(absPath);
log(`[open] invoked Module.kicadOpenFile(${absPath}) (async; polling for load)`);
}
/** Heuristic: the editor frame title drops "untitled" once a real file is open. */
function schematicLoaded(win: ToolWindow): boolean {
const title = win.document?.title ?? "";
return title.length > 0 && !/untitled/i.test(title);
}
export async function openFileInTool(
@ -125,9 +140,23 @@ export async function openFileInTool(
}
// Strategy 1: programmatic hook (preferred — deterministic, no UI automation).
if (tryProgrammaticOpen(win, absPath, log)) return "programmatic";
// Because the call is Asyncify-async we can't trust its return value; instead
// we invoke it and poll the frame title until the schematic loads. We must NOT
// fall back to UI automation while the hook is in flight — synthesizing input
// would re-enter the suspended Asyncify call and corrupt it.
if (hasProgrammaticHook(win)) {
invokeProgrammaticOpen(win, absPath, log);
const loaded = await waitFor(() => schematicLoaded(win), timeoutMs);
if (loaded) {
log(`[open] schematic loaded: ${win.document.title}`);
return "programmatic";
}
log("[open] kicadOpenFile did not load the schematic within timeout");
return "failed";
}
// Strategy 2: UI automation fallback (EXPERIMENTAL, fragile).
// Strategy 2: UI automation fallback (EXPERIMENTAL, fragile). Only when the
// build has no programmatic hook at all.
log("[open] no programmatic hook; using EXPERIMENTAL UI automation");
const fileMenu =

@ -1 +1 @@
Subproject commit 651526b80e08ed1fdfe0455925fc2fb90d8668bd
Subproject commit ea186374a59347955c42a5acaea8148ad855f0d3