Closed tompng closed 1 month ago
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?
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.
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:
TERM=dumb
in terminal emulator that does not support cursor position reportPTY.spawn
Terminal Emulator
Pseudo terminal launched by
PTY.spawn