mailbox S6: shutdown() in the shim + unit gates; bump wxwidgets
Shim shutdown: dead latch, queue rejection/drop with beacons, pump stops, idempotent. Gates: shim units 11/11, asyncify 9/9, coroutine 39/39, wx modal-heavy 45/45, kicad 6/6 — all on DEFAULT-injected glue (docker postprocess -> setup:kicad now yields scheduler builds without manual conversion). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
This commit is contained in:
parent
a17d87a4bd
commit
9bc4da89ff
4 changed files with 92 additions and 1 deletions
|
|
@ -290,6 +290,18 @@ rollback = the `WX_SCHEDULER=0` build + a per-step tag.
|
||||||
> consume-once layer is the backstop, not a replacement.
|
> consume-once layer is the backstop, not a replacement.
|
||||||
- **S6 · Lifetime (few d).** Cleanup ordering vs the scheduler; teardown deferred to
|
- **S6 · Lifetime (few d).** Cleanup ordering vs the scheduler; teardown deferred to
|
||||||
unload/explicit exit; `ScheduleExit` → scheduler wake (12 §phase-4).
|
unload/explicit exit; `ScheduleExit` → scheduler wake (12 §phase-4).
|
||||||
|
> **Work log 2026-08-05 — S6 LANDED.** The scheduler latches DEAD when the main loop
|
||||||
|
> exits (DoRun's top-level return → `shutdown("main loop exited")`): queued mutators
|
||||||
|
> reject, queued messages/wakes drop, pumps stop — stranded work beacons
|
||||||
|
> (`shutdown ... stranded:`) instead of surfacing as a post-teardown UAF.
|
||||||
|
> `wxWasmMailboxDeliver` gained ProcessEvents' `!wxTheApp` teardown parity. Unit-gated
|
||||||
|
> (scheduler-shim.test.ts 4/4 incl. shutdown; 11/11 with WasmMailbox) + full battery
|
||||||
|
> (asyncify 9/9, coroutine 39/39, wx modal-heavy 45/45) + kicad 6/6 — all on
|
||||||
|
> DEFAULT-injected glue: the docker postprocess → setup:kicad pipeline now produces
|
||||||
|
> scheduler builds with no manual conversion. Deferred with the ledger: OnExit/
|
||||||
|
> wxEntryCleanupReal-to-unload reshaping (12 §phase-4's fuller lifetime) — the current
|
||||||
|
> quit flow (wxAppTopWindowClosed → loop exit → cleanup) plus the DEAD latch covers the
|
||||||
|
> teardown-delivery hazard the step exists for.
|
||||||
|
|
||||||
**Effort: ≈ 5–7 weeks** (doc 12 said 4–6 for the runtime alone; the added week is §3's test
|
**Effort: ≈ 5–7 weeks** (doc 12 said 4–6 for the runtime alone; the added week is §3's test
|
||||||
work, which is where the safety comes from). S1 and S2 each end in a shippable state; S4 is the
|
work, which is where the safety comes from). S1 and S2 each end in a shippable state; S4 is the
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||||
enqueueAfter: function (fn, arg, ms) {
|
enqueueAfter: function (fn, arg, ms) {
|
||||||
var self = this;
|
var self = this;
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
|
if (self.dead) return; // S6: never deliver into a torn-down app
|
||||||
self.mailbox.push({ fn: fn, arg: arg });
|
self.mailbox.push({ fn: fn, arg: arg });
|
||||||
self.enqueued++;
|
self.enqueued++;
|
||||||
self._armDeliveryTick();
|
self._armDeliveryTick();
|
||||||
|
|
@ -56,6 +57,7 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||||
this._tickArmed = true;
|
this._tickArmed = true;
|
||||||
var self = this;
|
var self = this;
|
||||||
setTimeout(function tick() {
|
setTimeout(function tick() {
|
||||||
|
if (self.dead) { self._tickArmed = false; return; }
|
||||||
try {
|
try {
|
||||||
if (Module["_wxWasmMailboxTick"]) Module["_wxWasmMailboxTick"]();
|
if (Module["_wxWasmMailboxTick"]) Module["_wxWasmMailboxTick"]();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -123,6 +125,7 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||||
? function () { return performance.now(); }
|
? function () { return performance.now(); }
|
||||||
: function () { return Date.now(); };
|
: function () { return Date.now(); };
|
||||||
setTimeout(function pump() {
|
setTimeout(function pump() {
|
||||||
|
if (self.dead) { self._mutatorPumpArmed = false; return; }
|
||||||
// Unkillable: an exception escaping this body would end the setTimeout
|
// Unkillable: an exception escaping this body would end the setTimeout
|
||||||
// chain and wedge the queue forever (observed: 559 frozen messages).
|
// chain and wedge the queue forever (observed: 559 frozen messages).
|
||||||
try {
|
try {
|
||||||
|
|
@ -222,8 +225,42 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||||
try { return fn(); } finally { this._authorizedWrite--; }
|
try { return fn(); } finally { this._authorizedWrite--; }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- S6 lifetime --------------------------------------------------------
|
||||||
|
// Called when the wx main loop exits (DoRun's top-level return path). The
|
||||||
|
// app object is about to be destroyed: delivering anything after this
|
||||||
|
// point runs callbacks into freed C++ state. Queued mutators reject,
|
||||||
|
// queued messages and wakes drop — loudly, so a teardown that strands
|
||||||
|
// work is visible in the console instead of surfacing as a later UAF.
|
||||||
|
dead: false,
|
||||||
|
shutdown: function (reason) {
|
||||||
|
if (this.dead) return;
|
||||||
|
this.dead = true;
|
||||||
|
var stranded = {
|
||||||
|
mailbox: this.mailbox.length,
|
||||||
|
mutators: this.mutatorQueue.length,
|
||||||
|
wakes: this.readyWakes.length,
|
||||||
|
waits: this.waits.size,
|
||||||
|
};
|
||||||
|
this.mailbox.length = 0;
|
||||||
|
for (var i = 0; i < this.mutatorQueue.length; i++) {
|
||||||
|
try { this.mutatorQueue[i].reject(new Error("[wx-scheduler] shutdown: " + reason)); } catch (e) {}
|
||||||
|
}
|
||||||
|
this.mutatorQueue.length = 0;
|
||||||
|
this.readyWakes.length = 0;
|
||||||
|
if (stranded.mailbox || stranded.mutators || stranded.wakes || stranded.waits) {
|
||||||
|
console.warn("[wx-scheduler] shutdown (" + reason + ") stranded:"
|
||||||
|
+ " mailbox=" + stranded.mailbox
|
||||||
|
+ " mutators=" + stranded.mutators
|
||||||
|
+ " wakes=" + stranded.wakes
|
||||||
|
+ " pendingWaits=" + stranded.waits);
|
||||||
|
} else {
|
||||||
|
console.log("[wx-scheduler] shutdown (" + reason + ") clean");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
state: function () {
|
state: function () {
|
||||||
return "[wx-scheduler] build=1 impl=S2-core"
|
return "[wx-scheduler] build=1 impl=S2-core"
|
||||||
|
+ (this.dead ? " DEAD" : "")
|
||||||
+ " mailbox=" + this.mailbox.length
|
+ " mailbox=" + this.mailbox.length
|
||||||
+ " enqueued=" + this.enqueued
|
+ " enqueued=" + this.enqueued
|
||||||
+ " delivered=" + this.delivered
|
+ " delivered=" + this.delivered
|
||||||
|
|
@ -355,6 +392,7 @@ if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||||
var self = this;
|
var self = this;
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
self._wakeDrainArmed = false;
|
self._wakeDrainArmed = false;
|
||||||
|
if (self.dead) return; // S6: parked stacks are gone with the app
|
||||||
// Deliver from a CLEAN macrotask (export stack empty by construction).
|
// Deliver from a CLEAN macrotask (export stack empty by construction).
|
||||||
while (self.readyWakes.length > 0 && __transitionFree()) {
|
while (self.readyWakes.length > 0 && __transitionFree()) {
|
||||||
var w = self.readyWakes.shift();
|
var w = self.readyWakes.shift();
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,16 @@ const SHIM_PATH = path.resolve(
|
||||||
);
|
);
|
||||||
|
|
||||||
type SchedulerShape = {
|
type SchedulerShape = {
|
||||||
|
mailbox: unknown[];
|
||||||
mutatorQueue: unknown[];
|
mutatorQueue: unknown[];
|
||||||
mutatorsDelivered: number;
|
mutatorsDelivered: number;
|
||||||
readyWakes: { deliver: (r: unknown) => void; result: unknown }[];
|
readyWakes: { deliver: (r: unknown) => void; result: unknown }[];
|
||||||
deferredWakes: number;
|
deferredWakes: number;
|
||||||
drainedWakes: number;
|
drainedWakes: number;
|
||||||
strayWrites: number;
|
strayWrites: number;
|
||||||
|
dead: boolean;
|
||||||
|
shutdown(reason: string): void;
|
||||||
|
enqueueAfter(fn: number, arg: number, ms: number): void;
|
||||||
_openBusy(): boolean;
|
_openBusy(): boolean;
|
||||||
_armMutatorPump(): void;
|
_armMutatorPump(): void;
|
||||||
_scheduleWakeDrain(): void;
|
_scheduleWakeDrain(): void;
|
||||||
|
|
@ -136,6 +140,43 @@ describe("N5: scheduler shim under flood", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("S6 shutdown: queued mutators reject, messages and wakes drop, pumps stop", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
let busy = true;
|
||||||
|
const S = loadShim({ busy: () => busy });
|
||||||
|
const M = (globalThis as Record<string, unknown>).Module as {
|
||||||
|
kicadCollabApplyItems: (x: number) => Promise<string> | string;
|
||||||
|
};
|
||||||
|
const p = Promise.resolve(M.kicadCollabApplyItems(1));
|
||||||
|
const exP = expect(p).rejects.toThrow("shutdown");
|
||||||
|
S.enqueueAfter(1234, 0, 5);
|
||||||
|
await vi.advanceTimersByTimeAsync(6); // message lands in the mailbox
|
||||||
|
S.readyWakes.push({ deliver: () => undefined, result: 0 });
|
||||||
|
expect(S.mailbox.length).toBe(1);
|
||||||
|
expect(S.mutatorQueue.length).toBe(1);
|
||||||
|
|
||||||
|
S.shutdown("test teardown");
|
||||||
|
await exP;
|
||||||
|
expect(S.mailbox.length).toBe(0);
|
||||||
|
expect(S.mutatorQueue.length).toBe(0);
|
||||||
|
expect(S.readyWakes.length).toBe(0);
|
||||||
|
expect(S.dead).toBe(true);
|
||||||
|
expect(S.state()).toContain("DEAD");
|
||||||
|
|
||||||
|
// Post-shutdown enqueues are dropped, and idempotent shutdown is safe.
|
||||||
|
S.enqueueAfter(1234, 0, 1);
|
||||||
|
await vi.advanceTimersByTimeAsync(5);
|
||||||
|
expect(S.mailbox.length).toBe(0);
|
||||||
|
S.shutdown("again");
|
||||||
|
busy = false;
|
||||||
|
await vi.advanceTimersByTimeAsync(100); // pumps must stay stopped
|
||||||
|
expect(S.mutatorsDelivered).toBe(0);
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("deferred wakes drain strictly FIFO", async () => {
|
it("deferred wakes drain strictly FIFO", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1953c18527e92204e1bbc957a90922f211937613
|
Subproject commit a395abd019b733ba186543ff668a068e461abd96
|
||||||
Loading…
Reference in a new issue