Check signals in loops that dispatch no commands - #385
Conversation
The per-command signal check (Jim_CheckSignal after JimInvokeCommand)
is the only point at which a pending signal aborts a running script.
A loop whose body dispatches no command never reaches it, so
catch -signal {} (and interactive SIGINT) cannot break out of, e.g.:
while 1 {}
while {$i < $n} {incr i} ;# incr fast-path dispatches nothing
The empty-body and incr fast-paths in Jim_EvalObj return before the
command loop, and the C-level loop constructs do not check signals
themselves, so such loops spin until the process is killed.
Add a Jim_CheckSignal test at the iteration boundary of while, for
(both the general and optimized paths), loop, and foreach/lmap. When
no signal is pending the test is a single load of signal_level plus a
not-taken branch per iteration; command-dispatching loops, dominated
by dispatch cost, are unaffected.
Signed-off-by: David Giller <dgiller@pinnacle-exp.com>
|
I noticed a CI fail, so I wanted to check to make sure I didn't send you a Windows-incompatible patch. I ran it past Claude Fable, as I'm on the road at a trade show and don't have my whole environment with me or time for a deep dive myself this week. I wanted to offer the results in case they are useful. Do please note, even back at my desk I don't have a Windows build env, so while this "fix" is targeted on Windows, it's only been tested as not regressing on macOS (and assuming Linux would be the same). Discount accordingly. Executive summary: it seems the failing test is one you've been having trouble with on Windows. Fable believes that differences in timer granularity on the GitHub Windows CI containers combined with Windows CI build details are exposing a real issue and not a flawed test: an underlying issue in While I did a superficial review, I can't quite vouch for the analysis (unlike the PR), but it sounds plausible enough to me that I'm forwarding it on in case it is helpful to you. Patch is viewable at: ki3v/jimtcl@64bb005 Fable's explanation follows: The build-windows failure is
Since the docs say "If 'script' is given it is evaluated after each event", Tested on macOS (the HAVE_SELECT / precise-timer case): full suite green — If per-return evaluation is actually the intended semantic (the doc wording One adjacent observation, offered only in case it's useful: the missed — Claude Fable (analysis not yet vetted by Dave) |
|
I am not very up on GitHub Actions, but with Claude's help I enabled the CI on my fork and the Windows CI build/test seems to pass with the patch referenced above. |
The problem
A pending signal only aborts a running script at the per-command check (
Jim_CheckSignal, right afterJimInvokeCommandinJim_EvalObj) and atcatch/tryentry.But a loop whose body and condition dispatch no command reaches neither, so
catch -signal { ... }with an incoming signal likeSIGINTorALRMcan't break out of loops such as:The empty-body and
incrfast-paths inJim_EvalObjreturn before the command loop, and the C-level loop constructs (while,for,loop,foreach/lmap) don't check signals themselves, so these spin until the process is killed (with an untrappable signal).Degenerate cases, maybe. But they can arise in novice code, generated code, etc.
A signal already breaks any loop that dispatches a command each iteration in the body or the condition.
while {[ready]} {...}, or evenwhile {[expr {1+1}]} {}, are already interruptible because of the command substitution in the condition.The issue is loops where neither the condition nor the body dispatches a command, like a pure-expression condition with an empty or fast-path body such as the ones I list in the block above.
Reproduction
$ jimsh -e 'set i 0; signal handle ALRM; alarm 0.3; puts [catch -signal { while {$i < 4000000000} {incr i} } msg]/$msg' 5/SIGALRMWith the proposed change it prints
5/SIGALRM.On master that same line produces no output: the loop never checks the signal and runs for tens of seconds, completing the loop instead of breaking out at the
ALRM.The patch
I propose adding a
Jim_CheckSignaltest at the iteration boundary ofwhile,for(both the general and optimized paths),loop, andforeach/lmap. A pending signal breaks the loop withJIM_SIGNAL, propagated by the existing loop return-code handling.Jim_CheckSignalis the existing macro:Testing
make test, full suite, identical on master and with the patch:Performance
The concern is the fast loop paths, so we don't make them un-fast. The added check is one
signal_levelindirect load and a not-taken branch per iteration.Across 5 runs interleaving master and patched and totalling 15 minutes:
The last two come in under 1%, but some of the patched runs came in faster due to measurement jitter, and you can see that at any rate we are pulling numbers out of noise. I don't claim this to be a rigorous quantitative analysis.
For useful loops, I expect the cost is genuinely negligible.
Context
I'm experimenting with embedding Jim Tcl into a microcontroller project (Teensy 4.1 platform, bare metal) as a configuration shell and for user script callbacks in the application.
Here, I'm overriding
Jim_CheckSignalso it also calls a hook where I can pump my background services loop tightly. These empty loop cases could lead to starvation on my setup and eventually a watchdog reboot.I also noticed they cause signal deafness on the OS-hosted case, so I wanted to propose this patch back upstream.