a-shell icon indicating copy to clipboard operation
a-shell copied to clipboard

Rich BUG: keyboard not responding after text scroll components are closed, hanging both program and terminal until we quit a-Shell

Open Emasoft opened this issue 2 years ago • 18 comments
trafficstars

Here is another annoying bug that comes out often when using RICH. Using any textual or rich component that uses internal scrolling, like Scrollview, Console.Pager, or CodeBrowser, etc. will cause the following:

  • If the function runs in the main thread, the moment we close the module the entire script stop working. Not because of a crash or becouse has frozen, but because the keyboard doesn't write anything more. It is like lost. Is not clear if the issue is that the keyboard input is sent elswhere or because the keyboard is not writing at all. The input command in python stll blinks the cursor waiting, but nothing is written and to close the script one has to kill a-Shell.

  • if the function runs in a different thread, the stop to the keyboard or screen (still not clear) happens the same but the module before hanging gives the following error about signal not running in secondary threads:

Opening book..

Waiting for the thread to finish task...
╭───────────────────────────────────── Traceback (most recent call last) ─────────────────────────────────────╮
│ /var/mobile/Containers/Data/Application/5896FDC4-A93C-4F95-B6E0-41EAF01171C3/Library/lib/python3.11/site-pa │
│ ckages/textual/app.py:2076 in _process_messages                                                             │
│                                                                                                             │
│   2073 │   │   │   )                                                                                        │
│   2074 │   │   │                                                                                            │
│   2075 │   │   │   if not self._exit:                                                                       │
│ ❱ 2076 │   │   │   │   driver.start_application_mode()                                                      │
│   2077 │   │   │   │   try:                                                                                 │
│   2078 │   │   │   │   │   with redirect_stdout(self._capture_stdout):                                      │
│   2079 │   │   │   │   │   │   with redirect_stderr(self._capture_stderr):                                  │
│                                                                                                             │
│ ╭──────────────────────────────────────────────── locals ─────────────────────────────────────────────────╮ │
│ │                  css = 'App {\n        background: $background;\n        color: $text;\n    }\n\n       │ │
│ │                        *:disabl'+43                                                                     │ │
│ │               driver = <LinuxDriver CodeBrowser(title='CodeBrowser', classes={'-dark-mode'})>           │ │
│ │         driver_class = <class 'textual.drivers.linux_driver.LinuxDriver'>                               │ │
│ │                error = ValueError('signal only works in main thread of the main interpreter')           │ │
│ │             headless = False                                                                            │ │
│ │           load_event = Load()                                                                           │ │
│ │         message_hook = None                                                                             │ │
│ │                 path = '/var/mobile/Containers/Data/Application/5896FDC4-A93C-4F95-B6E0-41EAF01171C3/L… │ │
│ │       ready_callback = None                                                                             │ │
│ │ run_process_messages = <function App._process_messages.<locals>.run_process_messages at 0x12a6c1c60>    │ │
│ │                 self = CodeBrowser(title='CodeBrowser', classes={'-dark-mode'})                         │ │
│ │        terminal_size = None                                                                             │ │
│ │          tie_breaker = -1                                                                               │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                             │
│ /var/mobile/Containers/Data/Application/5896FDC4-A93C-4F95-B6E0-41EAF01171C3/Library/lib/python3.11/site-pa │
│ ckages/textual/drivers/linux_driver.py:136 in start_application_mode                                        │
│                                                                                                             │
│   133 │   │   def on_terminal_resize(signum, stack) -> None:                                                │
│   134 │   │   │   send_size_event()                                                                         │
│   135 │   │                                                                                                 │
│ ❱ 136 │   │   signal.signal(signal.SIGWINCH, on_terminal_resize)                                            │
│   137 │   │                                                                                                 │
│   138 │   │   self.write("\x1b[?1049h")  # Alt screen                                                       │
│   139                                                                                                       │
│                                                                                                             │
│ ╭──────────────────────────────────────────────── locals ─────────────────────────────────────────────────╮ │
│ │               loop = <_UnixSelectorEventLoop running=True closed=False debug=False>                     │ │
│ │ on_terminal_resize = <function LinuxDriver.start_application_mode.<locals>.on_terminal_resize at        │ │
│ │                      0x12a6c0d60>                                                                       │ │
│ │               self = <LinuxDriver CodeBrowser(title='CodeBrowser', classes={'-dark-mode'})>             │ │
│ │    send_size_event = <function LinuxDriver.start_application_mode.<locals>.send_size_event at           │ │
│ │                      0x12a6c1ee0>                                                                       │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                             │
│ /private/var/containers/Bundle/Application/87621026-4603-46BA-9B34-2EE4014FF869/a-Shell.app/Library/lib/pyt │
│ hon3.11/signal.py:56 in signal                                                                              │
│                                                                                                             │
│   53                                                                                                        │
│   54 @_wraps(_signal.signal)                                                                                │
│   55 def signal(signalnum, handler):                                                                        │
│ ❱ 56 │   handler = _signal.signal(_enum_to_int(signalnum), _enum_to_int(handler))                           │
│   57 │   return _int_to_enum(handler, Handlers)                                                             │
│   58                                                                                                        │
│   59                                                                                                        │
│                                                                                                             │
│ ╭─────────────────────────────────────────────── locals ───────────────────────────────────────────────╮    │
│ │   handler = <function LinuxDriver.start_application_mode.<locals>.on_terminal_resize at 0x12a6c0d60> │    │
│ │ signalnum = <Signals.SIGWINCH: 28>                                                                   │    │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯    │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
ValueError: signal only works in main thread of the main interpreter
Hello, World!
YOU ARE EXPERIENCING THE MYSTERIOUS INVISIBLE WRITING BUG. PROGRAM IS UNRESPONSIVE.
What is [i]your[/i] [bold red]name[/]? :smiley: 

I noticed another thing: somehow you need to load a long txt document, at least 2-3000 chars for it to happens. I don't know if this happen to be linked with the screen buffers or secondary layers sizes (maybe the text is written somewhere outside the screen?) but it is surely an issue. Since it depends but how each rich or textual component handle the off screen texts and by how big the text is, and by the user settings for the terminal size of a-Shell, it is not easy to replicate in a deterministic way, but it happens pretty often.

Sadly, many of the awesome progress meters, popup completion widgets, changing rich components, percentage completition bars, scroll tables, headers and footers, input panels, and other similar components are still non working in a-Shell or are seriously messed up graphically. At least 70% of the rich and textual functionalities are non working. Being the console of a-Shell a simulated replica of the original, of course supporting a-Shell completely would be a big deal, but being so central and transversal to so many python packages, I think it would be wise to put it among the priorities. Thanks.

Emasoft avatar Aug 18 '23 07:08 Emasoft

Can you provide a minimum script that I can use to reproduce the issues?

holzschu avatar Aug 18 '23 07:08 holzschu

@holzschu Yes, it took me a while to reduce the thing to a small source python file that reproduced the bug. I had to cut many things. Just run it in a-shell after installing the requirements (I provided the txt). Use only one argument when launching from the command line, a text file or markdown file decently long. I provided the demo.md in the zip, it gives me the issue, but as I said it depends on screen terminal size, res, memory, etc. If doesn't give the bug, try with something bigger.

test_code_for_rich_ashell_issues.zip

NOTE: Don't waste time trying to remove the env context switcher (I just left it to avoid changing your env permanently), or to try other key input libs, I already excluded them along with many other things. Let me know.

Emasoft avatar Aug 18 '23 16:08 Emasoft

To test the most suspect cause, I need to know what ANSI control codes are supported by a-Shell. Apparently, rich uses the ENABLE_ALT_SCREEN and DISABLE_ALT_SCREEN control sitring, as you can see here:

CONTROL_CODES_FORMAT: Dict[int, Callable[..., str]] = {
    ControlType.BELL: lambda: "\x07",
    ControlType.CARRIAGE_RETURN: lambda: "\r",
    ControlType.HOME: lambda: "\x1b[H",
    ControlType.CLEAR: lambda: "\x1b[2J",
    ControlType.ENABLE_ALT_SCREEN: lambda: "\x1b[?1049h",
    ControlType.DISABLE_ALT_SCREEN: lambda: "\x1b[?1049l",
    ControlType.SHOW_CURSOR: lambda: "\x1b[?25h",
    ControlType.HIDE_CURSOR: lambda: "\x1b[?25l",
    ControlType.CURSOR_UP: lambda param: f"\x1b[{param}A",
    ControlType.CURSOR_DOWN: lambda param: f"\x1b[{param}B",
    ControlType.CURSOR_FORWARD: lambda param: f"\x1b[{param}C",
    ControlType.CURSOR_BACKWARD: lambda param: f"\x1b[{param}D",
    ControlType.CURSOR_MOVE_TO_COLUMN: lambda param: f"\x1b[{param+1}G",
    ControlType.ERASE_IN_LINE: lambda param: f"\x1b[{param}K",
    ControlType.CURSOR_MOVE_TO: lambda x, y: f"\x1b[{y+1};{x+1}H",
    ControlType.SET_WINDOW_TITLE: lambda title: f"\x1b]0;{title}\x07",
}

https://github.com/Textualize/rich/blob/720800e6930d85ad027b1e9bd0cbb96b5e994ce3/rich/control.py#L34-L51

Emasoft avatar Aug 19 '23 17:08 Emasoft

a-Shell uses hterm.org for the handling of ANSI control codes, so all the supported codes are here: https://chromium.googlesource.com/apps/libapps/+/HEAD/hterm/docs/ControlSequences.md

Enable-alternate-screen is also used by vim, less and man, so it should work (I recongnize than 1049h).

holzschu avatar Aug 19 '23 18:08 holzschu

So why does the terminal stop responding?

Emasoft avatar Aug 23 '23 00:08 Emasoft

I think I have the reason, but not yet the solution. a-Shell separates between "interactive commands", that take care of the keyboard input and decide what to do with it, and "non-interactive commands", that let a-Shell take care of the command line. Vim, less, more, ipython... go into the first category. So does textual.

So a-Shell is forwarding every keyboard input to textual and trusts it to do what needs to be done. But with input(), you go back to non-interactive command: you want a-Shell to take care of the command line, and a-Shell is not aware of the change.

I see several solutions; the easier is using a more advanced Python input method, like the ones from prompt-toolkit. They will work because they are interactive: https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html

holzschu avatar Aug 23 '23 15:08 holzschu

I tried them already. Both rich console.input() and prompt.toolkit prompt() show the same issue. But you should remember that the issue is not happening all the time. Some times calling input() works perfectly fine. Something deeper is at play here. The stdin and stdout may be hijacked by a different thread, for example. But I don't know a-Shell well enough to debug this...

Emasoft avatar Aug 23 '23 21:08 Emasoft

prompt-toolkit worked for me, in a situation where input didn't work. I'm going to run more tests, but it still looks like the difference between interactive/non-interactive.

holzschu avatar Aug 24 '23 05:08 holzschu

I've been running more tests. I've never seen the issue with prompt(), I always see it with console.input(). Of course, prompt() does not have the fancy typesetting of Rich.

holzschu avatar Aug 26 '23 12:08 holzschu

While I was trying other input libraries, I found this bug with the readchar lib:

       k = readkey()
        ^^^^^^^^^
  File "/var/mobile/Containers/Data/Application/5896FDC4-A93C-4F95-B6E0-41EAF01171C3/Library/lib/python3.11/site-packages/readchar/_posix_read.py", line 34, in readkey
    c1 = readchar()
         ^^^^^^^^^^
  File "/var/mobile/Containers/Data/Application/5896FDC4-A93C-4F95-B6E0-41EAF01171C3/Library/lib/python3.11/site-packages/readchar/_posix_read.py", line 18, in readchar
    old_settings = termios.tcgetattr(fd)
                   ^^^^^^^^^^^^^^^^^^^^^
termios.error: (25, 'Inappropriate ioctl for device')

It seems that a-Shell has a bug related to termios. Is this bug related in any way to our issue?

update: I've found the same issue with two other libs: getkey and getchar

Emasoft avatar Aug 26 '23 15:08 Emasoft

@holzschu any update on this?

Emasoft avatar Aug 29 '23 21:08 Emasoft

I'm focusing (for now) on the update for the "basic" Python packages and then Python 3.12 itself. It's a bit time-consuming. The short answer is "I don't know".

holzschu avatar Aug 30 '23 10:08 holzschu

I ran into this issue today too, seems like you already have it figured out, but I found an even more minimal example using just the shell (in other words this is not python exclusive).

Running sh -c “vim && sh” leaves the keyboard unusable in the second dash shell.

I resetting the terminal back to a sane state with stty, but this does not seem to work

user18130814200115-2 avatar Oct 18 '23 18:10 user18130814200115-2

That makes sense, if you read above the distinction between interactive and non-interactive commands: https://github.com/holzschu/a-shell/issues/687#issuecomment-1690208058 I have to say, I don't see a reason why you would run a command like sh -c “vim && sh”. I understand the need for Rich and textual and I'm going to see if I can fix the behaviour, but running two interactive commands in sequence feels... weird.

holzschu avatar Oct 18 '23 20:10 holzschu

Oh I agree this was just the most minimal example that came up in my troubleshooting.

I’m actually trying to launch vim and return to an interactive python shell.

user18130814200115-2 avatar Oct 18 '23 20:10 user18130814200115-2

Anything that launches two interactive commands in sequence without returning to the prompt in between is going to run into problems. If you have an iPad, it's better to use the multiple windows to run Vim in one window and ipython in the other. With an iPhone, I would suggest the python mode in Vim or the embedded shell (:VimShell).

holzschu avatar Oct 19 '23 07:10 holzschu

I understand the need for Rich and textual and I'm going to see if I can fix the behaviour.

Thank you, that would be a lifesaver! 👍

Emasoft avatar Oct 19 '23 12:10 Emasoft

I gave it another try. I'm still unable to reproduce this issue, as long as I use prompt_toolkit.prompt.

from prompt_toolkit import prompt 

...

console.print("What is [i]your[/i] [bold red]name[/]? :smiley: ")
name = prompt()
console.print("What is your name? :smiley: ")
otherName = prompt()

This always returns on my machine, never freezes. I tried it with the man page for less (81 KB).

holzschu avatar Dec 20 '23 15:12 holzschu