Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a timeout to cursor_pos and warn if terminal does not respond to cursor position query #674

Open
tompng opened this issue Apr 7, 2024 · 2 comments
Labels
enhancement New feature or request

Comments

@tompng
Copy link
Member

tompng commented Apr 7, 2024

Description

If terminal emulator does not respond to cursor position query "\e[6n", Reline will wait forever with displayed in terminal.
Reline can add a timeout to at least work in such case. Warning message will help debugging.

Example:

  • Forgot to set TERM=dumb in terminal emulator that does not support cursor position report
  • Testing console with PTY.spawn

Terminal Emulator

Pseudo terminal launched by PTY.spawn

@tompng tompng changed the title Add a timeout to cursor_pos and warn if terminal does not respond to cursor position report Add a timeout to cursor_pos and warn if terminal does not respond to cursor position query Apr 7, 2024
@tompng tompng added the enhancement New feature or request label Apr 7, 2024
@etiennebarrie
Copy link
Contributor

Ah I was about to create an issue for this. Here's a repro script:

if ARGV.first == "prompt"
  require "readline"

  puts Readline.name

  puts Readline.readline("Prompt: ").upcase

else
  require "pty"

  output = ""
  PTY.spawn("ruby", __FILE__, "prompt") do |read, write, pid|
    loop do
      output += read.readpartial(4096)
      $stderr.puts(output.inspect)
      $stderr.puts(output)
      write.puts("hello") if output.end_with?(": ")
    rescue EOFError, SystemCallError
      Process.waitpid(pid)
      break
    end
  end
  puts
  puts "Output:", output
end

You can test this with a real terminal (podman can be replaced by docker):

$ podman run --rm -ti -v $PWD:/app ruby:3.2 ruby /app/bug.rb prompt
Readline
Prompt: readline
READLINE
$ podman run --rm -ti -v $PWD:/app ruby:3.3 ruby /app/bug.rb prompt
Reline
Prompt: reline
RELINE

But if we use PTY.spawn, it waits forever:

$ podman run --rm -ti -v $PWD:/app ruby:3.2 ruby /app/bug.rb 
"Readline\r\n"
Readline
"Readline\r\n\e[?2004hPrompt: "
Readline
Prompt: 
"Readline\r\n\e[?2004hPrompt: hello\r\n\e[?2004l\rHELLO\r\n"
Readline
Prompt: hello
HELLO

Output:
Readline
Prompt: hello
HELLO
$ podman run --rm -ti -v $PWD:/app ruby:3.3 ruby /app/bug.rb 
"Reline\r\n"
Reline
"Reline\r\n\e[1G\xE2\x96\xBD\e[6n"
Reline

^[[37;2R^C/app/bug.rb:14:in `readpartial': Interrupt
	from /app/bug.rb:14:in `block (2 levels) in <main>'
	from <internal:kernel>:187:in `loop'
	from /app/bug.rb:13:in `block in <main>'
	from /app/bug.rb:12:in `spawn'
	from /app/bug.rb:12:in `<main>'

As you mentioned, we can see "\e[6n", then we can even see the result from the terminal ^[[37;2R, meaning line 37, col 2, but it's not read (readable?) by Reline.

Using TERM=dumb shows a different bug where the output is written multiple times, but at least it works:

$ podman run --rm -ti -v $PWD:/app -e TERM=dumb ruby:3.3 ruby /app/bug.rb 
"Reline\r\n"
Reline
"Reline\r\nPrompt: Prompt: "
Reline
Prompt: Prompt: 
"Reline\r\nPrompt: Prompt: hello\r\n"
Reline
Prompt: Prompt: hello
"Reline\r\nPrompt: Prompt: hello\r\nPrompt: hPrompt: h"
Reline
Prompt: Prompt: hello
Prompt: hPrompt: h
"Reline\r\nPrompt: Prompt: hello\r\nPrompt: hPrompt: hPrompt: hePrompt: hePrompt: helPrompt: hel"
Reline
Prompt: Prompt: hello
Prompt: hPrompt: hPrompt: hePrompt: hePrompt: helPrompt: hel
"Reline\r\nPrompt: Prompt: hello\r\nPrompt: hPrompt: hPrompt: hePrompt: hePrompt: helPrompt: helPrompt: hell"
Reline
Prompt: Prompt: hello
Prompt: hPrompt: hPrompt: hePrompt: hePrompt: helPrompt: helPrompt: hell
"Reline\r\nPrompt: Prompt: hello\r\nPrompt: hPrompt: hPrompt: hePrompt: hePrompt: helPrompt: helPrompt: hellPrompt: hellPrompt: hello"
Reline
Prompt: Prompt: hello
Prompt: hPrompt: hPrompt: hePrompt: hePrompt: helPrompt: helPrompt: hellPrompt: hellPrompt: hello
"Reline\r\nPrompt: Prompt: hello\r\nPrompt: hPrompt: hPrompt: hePrompt: hePrompt: helPrompt: helPrompt: hellPrompt: hellPrompt: helloPrompt: hello"
Reline
Prompt: Prompt: hello
Prompt: hPrompt: hPrompt: hePrompt: hePrompt: helPrompt: helPrompt: hellPrompt: hellPrompt: helloPrompt: hello
"Reline\r\nPrompt: Prompt: hello\r\nPrompt: hPrompt: hPrompt: hePrompt: hePrompt: helPrompt: helPrompt: hellPrompt: hellPrompt: helloPrompt: helloHELLO\r\n"
Reline
Prompt: Prompt: hello
Prompt: hPrompt: hPrompt: hePrompt: hePrompt: helPrompt: helPrompt: hellPrompt: hellPrompt: helloPrompt: helloHELLO

Output:
Reline
Prompt: Prompt: hello
Prompt: hPrompt: hPrompt: hePrompt: hePrompt: helPrompt: helPrompt: hellPrompt: hellPrompt: helloPrompt: helloHELLO

@tompng Do you know if there's a way to work around this without using TERM=dumb? I don't really understand how terminals work deeply, is this a bug from PTY.spawn? Is there a way to get PTY.spawn to write to the PTY what Reline expects at the other end of the device?

@tompng
Copy link
Member Author

tompng commented Apr 10, 2024

In my understanding, using PTY means that the program is expected to act like a terminal emulator.

PTY.spawn("ruby", __FILE__, "prompt") do |read, write, pid|
  # Spawned process can check if it is executed in terminal emulator by `IO#tty?`. When spawned by PTY, it returns true.
  loop do
    s = read.readpartial(4096)
    if s.include?("\e[6n")
      # If terminal emulator(==this program) receives "\e[6n", terminal emulator should report cursor position
      # Since this program is not a full terminal emulator, report dummy cursor position
      cursor_col, cursor_row = 1, 1
      # Or you can report real cursor position retrieved from current terminal, virtual terminal library, another process, from remote, whatever. PTY doesn't support this part.
      write.write "\e[#{cursor_row};#{cursor_col}R"
    end
    output += s
  end
end

This is too hard, so there is TERM=dumb to avoid it.
If you don't need to provide tty to the spawned process, you can use IO.popen(command, 'r+') instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Development

No branches or pull requests

2 participants