Skip to content

fix: make Ctrl+C cancellation deterministic in the bridge (#4) - #6

Merged
srgg merged 1 commit into
mainfrom
fix/issue-4-deterministic-bridge-cancellation
Jul 6, 2026
Merged

fix: make Ctrl+C cancellation deterministic in the bridge (#4)#6
srgg merged 1 commit into
mainfrom
fix/issue-4-deterministic-bridge-cancellation

Conversation

@srgg

@srgg srgg commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the hang where, after Ctrl+C with a device connected, an interactive
bridge script never stops (blim hangs forever at "Stopping script"). The
cooperative-cancellation count hook (LUA_MASKCOUNT) is not honored inside
LuaJIT-compiled traces
, so an FFI-hot script loop spins forever and
L.DoString never returns.

Cancellation is made deterministic at the API boundary instead of relying on
the hook.

Changes

  • blim.sleep raises on cancellation. On ctx.Done() it re-locks
    stateMutex (RaiseError walks the Lua stack — the SIGSEGV when a synchronous BLE read is called from inside a subscription callback #3 corruption class) and
    raises "script execution cancelled". This Go-side raise is not catchable by
    Lua pcall, so a polling loop — including blim.term.read_char, which sleeps
    on every step — aborts within one iteration.
  • Bounded post-cancellation wait. A script with no cancellable boundary (a
    pure JIT/FFI loop) can never be stopped cooperatively. safeExecuteScript
    now gives up after scriptCancellationTimeout, dumps goroutines, and returns
    ErrScriptCancellationTimeout instead of hanging forever.
  • Terminal restored on bridge exit. A script may switch the terminal to raw
    via blim.term.enable_raw; cancellation aborts it before disable_raw runs,
    leaving the shell stuck in raw mode. runBridge is split into a thin cobra
    wrapper and runBridgeCtx(ctx, ...); the latter snapshots the terminal on
    entry and restores it on exit (x/term), the way ssh does. The ctx split
    also lets the test drive cancellation without a process-global signal.

Tests

  • TestSleepRaisesOnContextCancellationblim.sleep aborts the script on
    cancellation (code after the sleep never runs).
  • TestExecuteScript2_CancellationTimeout — an unstoppable script yields
    ErrScriptCancellationTimeout (checked via errors.Is), not an unbounded hang.
  • TestBridgeRestoresTerminalOnExit — black-box: fake-tty stdin + mock device;
    a raw-mode script leaves the terminal restored after cancellation.
  • Verified: whole module -race -count=1 clean, go vet + gofmt clean.

Follow-ups (out of scope, documented)

  • Thread ctx into the other blocking BLE waits (characteristic.read/write,
    blim.subscribe's CCCD write) so cancellation returns immediately rather than
    after the op's own completion. Now a responsiveness nicety, not a hang —
    the bounded wait already caps the worst case.
  • Pre-existing unsynchronized access to scriptExecutionCtx (not introduced
    here).

Fixes #4

After Ctrl+C with a device connected, an interactive bridge script never
stopped: blim hung forever at "Stopping script". The cooperative-cancellation
count hook (LUA_MASKCOUNT) is not honored inside LuaJIT-compiled traces, so an
FFI-hot script loop spun forever and L.DoString never returned.

Make cancellation deterministic at the API boundary instead of relying on the
hook:

- blim.sleep now raises "script execution cancelled" on ctx.Done() (re-locking
  stateMutex before the raise, since RaiseError walks the Lua stack — the #3
  corruption class). This Go-side raise is not catchable by Lua pcall, so a
  polling loop (including blim.term.read_char, which sleeps on every step)
  aborts within one iteration.
- Bound the post-cancellation wait in safeExecuteScript: a script with no
  cancellable boundary (a pure JIT/FFI loop) can never be stopped
  cooperatively, so after scriptCancellationTimeout give up, dump goroutines,
  and return ErrScriptCancellationTimeout instead of hanging forever.
- Restore the terminal on bridge exit: a script may switch it to raw via
  blim.term.enable_raw and cancellation aborts before disable_raw runs, leaving
  the shell stuck in raw mode. runBridge is split into a thin cobra wrapper and
  runBridgeCtx(ctx, ...); the latter snapshots the terminal on entry and
  restores it on exit (x/term), the way ssh does. The ctx split also lets the
  test drive cancellation without a process-global signal.

Adds deterministic tests: blim.sleep raises on cancellation, the bounded wait
returns a timeout error for an unstoppable script, and a black-box bridge test
(fake-tty stdin + mock device) that a raw-mode script leaves the terminal
restored after cancellation.

Fixes #4
@srgg
srgg merged commit ffc511b into main Jul 6, 2026
1 check failed
@srgg
srgg deleted the fix/issue-4-deterministic-bridge-cancellation branch July 6, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ctrl+C hangs forever: cancellation count-hook is not honored in LuaJIT-compiled traces; blim.sleep should raise on ctx.Done

1 participant