## a recycled descriptor number must not inherit a readiness event ## ## The io thread reads multiplexer events into a buffer that the VM drains ## later, so unregistering a descriptor does not reach events already handed ## back. sys.pclose then closes the descriptor and the next sys.popen gets the ## same number, at which point a leftover event is delivered against the new ## pipe: the semaphore fires for something that was never ready and the read ## finds nothing. ## ## The child sleeps before writing, so a wakeup that arrives before the write ## is unmistakably spurious rather than merely early - which is what makes this ## deterministic instead of a race. Each round must read its own three bytes. fun chk(ok msg) { if ok { printf "OK: %s\n" msg } \ else { printf "ERROR: %s\n" msg } } iosem := (core.sem-new) tmo := (core.sem-new) sg := (core.semgr-new) core.semgr-add sg iosem core.semgr-add sg tmo ## 1 when the handle became readable, 0 when the timer won instead fun waitin(h secs) { | s | core.sem-signal tmo secs 0 core.sem-signal-on-input iosem h s := (core.semgr-wait sg) core.sem-unsignal iosem core.sem-unsignal tmo if (eqv? s tmo) { return 0 } else { return 1 } } fun round(n) { | p outh buf ready got | p := (sys.popen "sleep 0.4; echo hi" "r") outh := (core.basicAt p 2) ready := (waitin outh 5) buf := (core.basicNew ByteArray 8) got := (sys.read outh buf) chk (== ready 1) "the child's output was reported ready" ## -1 here means the wakeup belonged to a previous round's pipe chk (== got 3) "the ready descriptor really had the child's bytes" sys.pclose (core.basicAt p 0) } ## every round closes its pipe, so the next popen gets the same descriptor ## number back - that reuse is what exposes a stale event round 1 round 2 round 3 round 4