design-b D3: fix the doc-19 hang — quasi-modals off the coroutine stack
THE BUG IS FIXED. tests/kicad/quasimodal-strand.spec.ts flips from a test.fail() pin to a plain green regression test: 3/3 runs closed=true dialogs=0 refused-resumes=0 (was closed=false dialogs=1 refused-resumes=1 on every run). Mechanism: a quasi-modal's nested event loop parked on the TOOL COROUTINE's stack, which suspends the fiber's body where the fiber layer cannot see it — so the stale-fiber guard quarantined the fiber and then refused its own resume, the dispatch guard was never released, and every click after that was deferred forever. Bouncing the nested loop onto the main stack leaves the coroutine suspended the legitimate way (a recorded fiber swap), so nothing is quarantined and nothing is refused. Layering, so this is not a pile of WASM ifdefs in KiCad: - wx (3d37db3bf1) owns the POLICY and the hook; it must not know what a coroutine is. - wasm/bindings/main_stack_runner.h is the only place that may know both sides: it finds the frame's TOOL_MANAGER and bounces via RunMainStack. Header-only and self-installing, so no build-script change; included by every editor's binding TU. - KiCad gets ONE ifdef-free method (2c777efede), needed only because TOOL_STATE is opaque outside TOOL_MANAGER. libcontext and dialog_shim are untouched — an earlier draft edited both and was reverted. This also reframes the remaining plan: the doc-19 class is closed WITHOUT migrating tool coroutines onto scheduler contexts. Note it does not make the wait a context yield — waits still park in place, just never on a coroutine stack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
This commit is contained in:
parent
5d8d53b678
commit
48da418f34
10 changed files with 108 additions and 17 deletions
2
kicad
2
kicad
|
|
@ -1 +1 @@
|
|||
Subproject commit 4b1849424db8d3e0b0c5343b76034beafe106377
|
||||
Subproject commit 2c777efedee85b5ee5cd8e46fbd10736cecc1729
|
||||
|
|
@ -757,17 +757,16 @@ $(S)/coroutine-nested/nested_test.html: $(S)/coroutine-nested/nested_test.o $(S)
|
|||
../../scripts/common/inject-dyncall-shims.sh $(basename $@).js
|
||||
|
||||
# Asyncify race-condition red-green harness (tests/asyncify/). Three variants of
|
||||
# Scheduler-context harness (docs/features/async/20 D1, 21). Pure fibers +
|
||||
# Asyncify, no wx: the contexts layer under test is wx-independent.
|
||||
SCHED_CTX_DIR = ../../wasm/sched
|
||||
# Scheduler-context harness (docs/features/async/20 D1/D2, 21). The layer is
|
||||
# header-only in wx's wasm port (wx/wasm/private/sched_context.h — D2 puts wx's
|
||||
# own dispatch on it), so this app needs no wx LIBRARY, just the include path:
|
||||
# a failure here can still only be the contexts layer.
|
||||
SCHED_CTX_INC = ../../wxwidgets/include
|
||||
|
||||
$(S)/sched-context/sched_context_test.o: $(S)/sched-context/sched_context_test.cpp $(SCHED_CTX_DIR)/context.h
|
||||
$(CXX) -c $(CXXFLAGS) -I$(SCHED_CTX_DIR) $< -o $@
|
||||
$(S)/sched-context/sched_context_test.o: $(S)/sched-context/sched_context_test.cpp $(SCHED_CTX_INC)/wx/wasm/private/sched_context.h
|
||||
$(CXX) -c $(CXXFLAGS) -I$(SCHED_CTX_INC) $< -o $@
|
||||
|
||||
$(S)/sched-context/context.o: $(SCHED_CTX_DIR)/context.cpp $(SCHED_CTX_DIR)/context.h
|
||||
$(CXX) -c $(CXXFLAGS) -I$(SCHED_CTX_DIR) $< -o $@
|
||||
|
||||
$(S)/sched-context/sched_context_test.html: $(S)/sched-context/sched_context_test.o $(S)/sched-context/context.o
|
||||
$(S)/sched-context/sched_context_test.html: $(S)/sched-context/sched_context_test.o
|
||||
$(CXX) $^ $(LDFLAGS_SCHED_CTX) --shell-file $(HTML) -o $@
|
||||
../../scripts/common/inject-dyncall-shims.sh $(basename $@).js
|
||||
|
||||
|
|
|
|||
|
|
@ -310,15 +310,16 @@ test.describe("quasi-modal strand (doc 19)", () => {
|
|||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("doc-19 red: OK resolves the quasi-modal wait and the dialog closes", async ({
|
||||
test("doc-19: OK resolves the quasi-modal wait and the dialog closes", async ({
|
||||
page,
|
||||
testLogger,
|
||||
}) => {
|
||||
// RED today by the doc-19 strand (aliased wake → quarantined fiber →
|
||||
// refused resume → dialog never closes). Goes GREEN at D3 (waits become
|
||||
// scheduler-context yields); Playwright then reports "expected to fail
|
||||
// but passed" and this marker comes off.
|
||||
test.fail();
|
||||
// WAS RED (aliased wake → quarantined fiber → refused resume → the dialog
|
||||
// could never be closed by a click). GREEN since the quasi-modal's nested
|
||||
// event loop stopped running on the tool coroutine's stack: it is bounced
|
||||
// onto the main stack, so the coroutine is suspended the legitimate way —
|
||||
// a recorded fiber swap — instead of parking its body where the fiber
|
||||
// layer cannot see it. This test is now the regression pin for that.
|
||||
test.setTimeout(240000);
|
||||
await bootAndOpen(page);
|
||||
const { timer } = await openDialogAndEngageWindow(page);
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@
|
|||
#include <tool/coroutine.h>
|
||||
#include <pcbjam_remote_lock.h>
|
||||
#include "collab_common.h"
|
||||
#include "main_stack_runner.h"
|
||||
#include "open_gate.h"
|
||||
#include "collab_presence_core.h"
|
||||
#include "collab_presence_style.h"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
#include <vector>
|
||||
#include <wx/app.h>
|
||||
#include <wx/string.h>
|
||||
#include "main_stack_runner.h"
|
||||
#include "open_gate.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
#include <project.h>
|
||||
|
||||
#include "pcbjam_libs_reload.h"
|
||||
#include "main_stack_runner.h"
|
||||
#include "open_gate.h"
|
||||
#include "timer_park.h"
|
||||
#include "fiber_park.h"
|
||||
|
|
|
|||
86
wasm/bindings/main_stack_runner.h
Normal file
86
wasm/bindings/main_stack_runner.h
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Main-stack runner: the KiCad half of wx's nested-loop bounce
|
||||
* (pcbjam docs/features/async/19, 20 D3).
|
||||
*
|
||||
* A quasi-modal's nested event loop parks its whole stack for the dialog's
|
||||
* lifetime. When that stack is a TOOL coroutine's, the park suspends the
|
||||
* fiber's body where the fiber layer cannot see it: the stale-fiber guard
|
||||
* quarantines the fiber, then REFUSES its own resume, and the dialog stops
|
||||
* responding to clicks (only the titlebar x still works, because that path is
|
||||
* ungated). That is the Symbol Properties hang.
|
||||
*
|
||||
* wx detects "this nested loop is about to park on a non-main stack" — it can,
|
||||
* cheaply and exactly, by comparing a frame address against
|
||||
* emscripten_stack_get_base()/end() — but it must not know what a coroutine
|
||||
* is. So it calls this runner, and KiCad's TOOL_MANAGER moves the loop onto
|
||||
* the main stack via its own RunMainStack mechanism, which suspends the
|
||||
* coroutine the legitimate way: a fiber swap the layer records
|
||||
* (swap_suspended = true), so no quarantine, no refused resume.
|
||||
*
|
||||
* This lives in pcbjam's binding layer rather than in KiCad or wx precisely
|
||||
* because it is the only place that may know about both.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <wx/app.h>
|
||||
|
||||
#include <eda_base_frame.h>
|
||||
#include <tool/tool_manager.h>
|
||||
|
||||
#include <wx/wasm/private/mainstack.h>
|
||||
|
||||
namespace pcbjam_main_stack
|
||||
{
|
||||
|
||||
/**
|
||||
* Run aFunc on the main stack if a tool coroutine is currently active.
|
||||
*
|
||||
* Returns 1 when the body has been run (bounced, or run inline because there
|
||||
* was no coroutine to bounce off), 0 when there was nothing to run it with —
|
||||
* no frame or no tool manager — in which case wx parks in place exactly as it
|
||||
* did before this hook existed.
|
||||
*/
|
||||
inline int run_on_main_stack( void ( *aFunc )( void* ), void* aArg )
|
||||
{
|
||||
if( !wxTheApp )
|
||||
return 0;
|
||||
|
||||
auto* frame = dynamic_cast<EDA_BASE_FRAME*>( wxTheApp->GetTopWindow() );
|
||||
|
||||
if( !frame )
|
||||
return 0;
|
||||
|
||||
TOOL_MANAGER* toolMgr = frame->GetToolManager();
|
||||
|
||||
if( !toolMgr )
|
||||
return 0;
|
||||
|
||||
// RunOnMainStackIfActiveTool runs the body inline when no coroutine is
|
||||
// running. Either way the body HAS run, so report it as handled — letting
|
||||
// wx fall through would run the nested loop a second time.
|
||||
bool ran = false;
|
||||
|
||||
toolMgr->RunOnMainStackIfActiveTool(
|
||||
[aFunc, aArg, &ran]()
|
||||
{
|
||||
ran = true;
|
||||
aFunc( aArg );
|
||||
} );
|
||||
|
||||
return ran ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs at static-init time. Only stores a function pointer, so it is safe
|
||||
* before wx exists; every lookup above happens lazily per call, since frames
|
||||
* come and go. Included by more than one binding TU — setting the same pointer
|
||||
* twice is idempotent.
|
||||
*/
|
||||
struct INSTALLER
|
||||
{
|
||||
INSTALLER() { wxWasmSetMainStackRunner( &run_on_main_stack ); }
|
||||
};
|
||||
|
||||
inline INSTALLER g_installer;
|
||||
|
||||
} // namespace pcbjam_main_stack
|
||||
|
|
@ -52,6 +52,7 @@
|
|||
#include <nlohmann/json.hpp>
|
||||
#include "collab_common.h"
|
||||
#include "collab_presence_core.h"
|
||||
#include "main_stack_runner.h"
|
||||
#include "open_gate.h"
|
||||
#include "timer_park.h"
|
||||
#include "fiber_park.h"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include <wx/string.h>
|
||||
#include <wx/window.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "main_stack_runner.h"
|
||||
#include "open_gate.h"
|
||||
#include <eda_draw_frame.h>
|
||||
#include <kiid.h>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit fc762bedc7ac6cd93eba90e910236ef050681728
|
||||
Subproject commit 3d37db3bf1cdffaaefe657047d7f6129f81fdb01
|
||||
Loading…
Reference in a new issue