selectel / pyte

Simple VTXXX-compatible linux terminal emulator
http://pyte.readthedocs.org/
GNU Lesser General Public License v3.0
653 stars 101 forks source link

Handle dispatch failure #101

Closed acroz closed 6 years ago

acroz commented 6 years ago

This PR restarts the parser finite state machine if it exits with an exception. Prior to this change, failures in the handler would leave the stream in an unusable state, since calling .send() on the parser finite state machine would raise StopIteration.

superbobry commented 6 years ago

Thanks for another contribution!

I'm a bit cautious about adding any exception handling to Stream because of performance concerns (although this is for the benchmarks to confirm). In general, neither Stream nor Screen are not supposed to raise unless the input is corrupted in some way (or there's a bug in pyte). If this happens

With this in mind, I have a counter proposal: wdyt about just adding a reset to Stream? This way the users would be able to reset it, but Stream itself would not do any implicit automagic.

Also, do you have examples of real failures you want to mitigate by this PR?

acroz commented 6 years ago

Source of the problem

We discovered that when exiting a particular program (midnight commander), the control sequence 'CSI ? Pm r' (where Pm is a semicolon-delimited set of digits, see http://invisible-island.net/xterm/ctlseqs/ctlseqs.html) was being read from the terminal process and written into the pyte Stream.

The current implementation of the CSI dispatcher in the streams module assumes that the meaning of the message can be uniquely determined from the last character of the control sequence. In this case, the character r is determined to indicate DECSTBM i.e. set the scrolling region. This gets mapped to the set_margins method of Stream, which does not accept a private argument as DECSTBM does not come with a ? after the CSI.

However, 'CSI ? Pm r' is a valid sequence, indicating "Restore DEC Private Mode Values". pyte matches this with the set_margins() method as described above, and tries to call it with a private=True argument, which fails as it is not an expected argument.

This causes a TypeError to be raised, which is bubbled out to the caller of feed(), but it also causes the Stream._parser generator to exit, causing further calls to feed() to fail with StopIteration, even if the TypeError was caught. Here is a snippet demonstrating this:

import pyte

screen = pyte.Screen(10, 6)
stream = pyte.Stream()
stream.attach(screen)

# Set mode 2
stream.feed('\x1b[2h')

# Restore DEC private mode values
try:
    stream.feed('\x1b[?10r')
except Exception as e:
    print('Caught exception:')
    print(repr(e))

# Set mode 4
print('Trying to feed another control sequence:')
stream.feed('\x1b[4h')

Solution

I considered extending the CSI parser to disambiguate messages on the basis that they are private or not, and we should perhaps still do that, however we don't really care about handling the "Restore DEC private mode values" message - we just want the parser to keep working.

That the parser is implemented as a generator (a very nice design btw) is an implementation detail that I don't think the caller should require knowledge of. There could be other exceptions raised inside the listening screen's methods that could also crash the parser. The motivation of this PR was therefore to continue to bubble out exceptions raised when handling messages, while allowing the user to catch and ignore those messages, but continue to call feed() as before.

In response to your concerns:

Other implementations I considered were:

If you think one of these is better I'm happy to modify my PR accordingly.

uSpike commented 6 years ago

Hi, is there desire from the maintainers to merge these changes? It is affecting me since we are interfacing with SBCs and <100% reliable serial connections that drop characters from time-to-time.

acroz commented 6 years ago

I am happy to pick this up again if it helps, @superbobry

superbobry commented 6 years ago

Thanks everyone! I've merged the PR manually.