#1133

query_keyboard_enhancement_flags blocks forever when the terminal answers CSI ? u but not DA1

Openmroweopened 25d ago

Version: crossterm 0.29, still present on master (src/terminal/sys/unix.rs:255). Unix.

Summary

supports_keyboard_enhancement() / query_keyboard_enhancement_flags() blocks forever if the terminal replies to CSI ? u but never replies to Primary Device Attributes (CSI c).

The first reply is bounded by poll_internal(Some(Duration::from_millis(2000)), …), but once the keyboard-flags reply arrives the DA1 flush is an unbounded read:

Ok(InternalEvent::KeyboardEnhancementFlags(current_flags)) => {
    // Flush the PrimaryDeviceAttributes out of the event queue.
    internal::read(&PrimaryDeviceAttributesFilter).ok();
    return Ok(Some(current_flags));
}

Because this usually runs before an app draws anything and with raw mode enabled, the user sees a blank terminal that ignores every key including Ctrl-C.

Real-world case

Kelpi 0.2.12 answers CSI ? u with CSI ? 0 u but does not answer CSI c (benfriebe/kelpi#349). tuicr calls supports_keyboard_enhancement() at startup and hangs indefinitely there (agavra/tuicr#751). The terminal is at fault for skipping DA1, but a capability probe shouldn't be able to hang the app.

Repro

Run any binary that calls supports_keyboard_enhancement() in a pty that replies only to the keyboard query (shown here with tuicr):

import os, pty, select, time
pid, fd = pty.fork()
if pid == 0:
    os.execvp("tuicr", ["tuicr", "--no-update-check"])
out, sent, t0 = b"", False, time.time()
while time.time() - t0 < 5:
    if select.select([fd], [], [], 0.1)[0]:
        out += os.read(fd, 65536)
    if not sent and b"\x1b[c" in out:
        os.write(fd, b"\x1b[?0u"); sent = True   # no DA1 reply
print(len(out), repr(out[:80]))
os.kill(pid, 9)
Terminal repliesResult
CSI ? 0 u onlyblocks forever (still blocked after 5 s)
CSI ? 0 u + CSI ? 62 ; 22 creturns immediately
nothingreturns Err after the 2 s timeout

Suggested fix

Bound the DA1 flush the same way as the first read, e.g. poll_internal(Some(Duration::from_millis(2000)), &PrimaryDeviceAttributesFilter) and only read if it returns Ok(true). Returning Ok(Some(current_flags)) either way keeps the current result when DA1 is missing, just without the hang.

Activity

0

No activity yet.