If you’re writing a multithreaded Ruby program that uses ncurses,
you might be curious why program stops running when you call
Ncurses.getch. Sup has been plagued
by this issue since 2005. Thankfully, I think I finally understand
it.
The problem is that there is a bug in the Ruby ncurses library
such that using blocking input will block all Ruby threads when
it waits for user input, instead of just the calling thread. So
Ncurses.getch will cause everything to grind to a halt. This is
probably due to the library not releasing the GVL when blocking on
stdin.
This bug is present in the latest rubygems version of curses, 0.9.1. It has been fixed in the latest libncurses-ruby Debian packages (1.1-3).
To see if you have a buggy, blocking version of the ruby ncurses library, run this program:
require 'rubygems'
require 'ncurses'
require 'thread'
Ncurses.initscr
Ncurses.noecho
Ncurses.cbreak
Ncurses.curs_set 0
Thread.new do
sleep 0.1
Ncurses.stdscr.mvaddstr 0, 0, "library is GOOD."
end
begin
Ncurses.stdscr.mvaddstr 0, 0, "library is BAD."
Ncurses.getch
ensure
Ncurses.curs_set 1
Ncurses.endwin
puts "bye"
end
(I purposely require rubygems in there to load the rubygems
ncurses library if it’s present; you can drop this if you don’t
use rubygems.)
There are two workarounds to this problem. First, you can simply tell ncurses to use nonblocking input:
Ncurses.nodelay Ncurses.stdscr, true
But if you’re writing a multithreaded app, you probably aren’t interested in nonblocking input, unless you want a nasty polling loop.
The better choice is to add a call to IO.select before getch,
which will block the calling thread until there’s an actual
keypress, and then allow getch to pick it up:
if IO.select [$stdin], nil, nil, 1
Ncurses.getch
end
IO.select requires a delay, so you’ll have to handle the
periodic nils that generates. But the background threads should no longer block.
There is one further complication, which is that you won’t be able
to receive the pseudo-keypresses Ncurses emits when the terminal
size changes, since they don’t show up on $stdin and thus the
select won’t pass. The solution is to install your own signal
handler:
trap("WINCH") { ... handle sigwinch ... }
You will still see the resize events coming from getch, but only
once the user presses a key. You can drop them at this point.
That should be enough to make any multithreaded Ruby ncurses app
able function. Of course, once everyone’s using a fixed version fo
the ncurses libraries, you can do away with the select and set
nodelay to false.
(One last hint for the future: I’ve found it necessary to set it
to false before every call to getch; otherwise a ctrl-c will
magically change it back to nonblocking mode. Not sure why.)