Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions ext/io/console/console.c
Original file line number Diff line number Diff line change
Expand Up @@ -1914,6 +1914,103 @@ console_ttyname(VALUE io)
# define console_ttyname rb_f_notimplement
#endif

typedef enum {
platform_default,
#if defined _WIN32 || defined __CYGWIN__
platform_cygwin,
platform_msys,
#endif
platform_any,

platform_default_bit = 1U << platform_default,
#if defined _WIN32 || defined __CYGWIN__
platform_cygwin_bit = 1U << platform_cygwin,
platform_msys_bit = 1U << platform_msys,
#endif
platform_any_bit = (1U << platform_any) - 1 /* all bits */
} console_platform_t;

/*
* call-seq:
* io.tty?([mode, ...]) -> true or false
*
* Returns +true+ if the stream is associated with a terminal device (tty),
* +false+ otherwise.
*
* If one or more +type+s are given, returns +true+ if the stream is
* associated with any of the specified tty types.
*
* - +nil+ : Returns the result of the default tty check, as if no type were
* given. It can be combined with other types.
* - +:any+ : Returns +true+ for any known kind of tty, including the
* default tty.
* - +:cygwin+ : Returns +true+ for cygwin tty, on Windows.
* - +:msys+ : Returns +true+ for msys2 tty, on Windows.
*/
static VALUE
console_platform_tty_p(int argc, VALUE *argv, VALUE io)
{
VALUE ret = Qfalse;
int mode = 0;

if (argc > 0) {
int i;
for (i = 0; i < argc; ++i) {
VALUE m = argv[i];
if (NIL_P(m)) {
mode |= platform_default_bit;
continue;
}
Check_Type(m, T_SYMBOL);
if (m == ID2SYM(rb_intern("any"))) {
mode |= platform_any_bit;
}
#if defined _WIN32 || defined __CYGWIN__
else if (m == ID2SYM(rb_intern("cygwin"))) {
mode |= platform_cygwin_bit;
}
else if (m == ID2SYM(rb_intern("msys"))) {
mode |= platform_msys_bit;
}
#endif
else {
rb_raise(rb_eArgError, "unknown tty type: %+" PRIsVALUE, m);
}
}
}
if ((mode & platform_default_bit) || (mode == 0)) {
ret = rb_call_super(0, 0);
}
if ((mode & ~platform_default_bit) && !RTEST(ret)) {
#if defined _WIN32 || defined __CYGWIN__
if (mode & (platform_cygwin_bit | platform_msys_bit)) {
struct {
FILE_NAME_INFO info;
WCHAR rest[MAX_PATH];
} buffer;

HANDLE h = (HANDLE)rb_w32_get_osfhandle(GetReadFD(io));
if ((GetFileType(h) == FILE_TYPE_PIPE) &&
GetFileInformationByHandleEx(h, FileNameInfo, &buffer, sizeof(buffer))) {
WCHAR *const name = buffer.info.FileName;
DWORD len = buffer.info.FileNameLength / sizeof(WCHAR);
name[len] = L'\0';
# define tty_pipe_p(type) \
(memcmp(name, L"\\" #type "-", sizeof(L"\\" #type)) == 0 && \
wcsstr(&name[rb_strlen_lit("\\" #type "-")], L"-pty") != NULL)
if (!ret && (mode & platform_cygwin_bit)) {
ret = tty_pipe_p(cygwin);
}
if (!ret && (mode & platform_msys_bit)) {
ret = tty_pipe_p(msys);
}
}
}
#endif
}
return ret;
}

/*
* IO console methods
*/
Expand Down Expand Up @@ -1982,6 +2079,21 @@ InitVM_console(void)
rb_define_method(rb_cIO, "check_winsize_changed", console_check_winsize_changed, 0);
rb_define_method(rb_cIO, "getpass", console_getpass, -1);
rb_define_method(rb_cIO, "ttyname", console_ttyname, 0);
{
/* :stopdoc: */
VALUE platform = rb_define_module_under(rb_cIO, "platform_tty");
/* :startdoc: */
{
VALUE rb_cIO = platform;
rb_define_method(rb_cIO, "tty?", console_platform_tty_p, -1);
rb_define_method(rb_cIO, "isatty", console_platform_tty_p, -1);
}
#ifdef HAVE_RB_PREPEND_MODULE
rb_prepend_module(rb_cIO, platform);
#else
rb_funcall(rb_cIO, rb_intern_const("prepend"), 1, platform);
#endif
}
rb_define_singleton_method(rb_cIO, "console", console_dev, -1);
{
/* :nodoc: */
Expand Down
4 changes: 3 additions & 1 deletion ext/io/console/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
hdr = nil
case
when macro_defined?("_WIN32", "")
win32 = true
# rb_w32_map_errno: 1.8.7
vk_header = File.exist?("#$srcdir/win32_vk.list") ? "chksum" : "inc"
vk_header = "#{'{$(srcdir)}' if $nmake == ?m}win32_vk.#{vk_header}"
Expand All @@ -47,7 +48,8 @@
elsif have_func("rb_scheduler_timeout") # Ruby 3.0 (internal)
have_func("rb_io_wait") # Ruby 3.0
end
have_func("ttyname_r") or have_func("ttyname")
win32 or have_func("ttyname_r") or have_func("ttyname")
have_func("rb_prepend_module") # not exported by TruffleRuby
create_makefile("io/console") {|conf|
conf << "\n""VK_HEADER = #{vk_header}\n"
}
Expand Down
156 changes: 100 additions & 56 deletions test/io/console/test_io_console.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,37 @@ def test_bad_keyword
end
end
end

TTY_ENHANCED = IO.instance_method(:tty?).arity != 0

def test_tty?
pend "not supported" unless TTY_ENHANCED

tty = STDIN.tty?(:any)
assert_include([true, false], tty)
assert_equal(tty, STDIN.tty?(:any, :any))
assert_equal(tty, STDIN.tty?(nil, :any))
assert_equal(STDIN.tty?, STDIN.tty?(nil))
end

def test_tty_non_tty
pend "not supported" unless TTY_ENHANCED

File.open(IO::NULL) do |f|
assert_not_predicate(f, :tty?)
assert_not_operator(f, :tty?, :any)
assert_not_operator(f, :tty?, nil)
assert_not_send([f, :tty?, :any, :any])
assert_not_send([f, :tty?, nil, :any])

assert_raise(TypeError) {f.tty?("any")}
assert_raise(ArgumentError) {f.tty?(:unknown)}
end
end
end

defined?(PTY) and defined?(IO.console) and TestIO_Console.class_eval do
defined?(PTY) and defined?(IO.console) and \
class TestIO_Console
Bug6116 = '[ruby-dev:45309]'

def test_raw
Expand Down Expand Up @@ -242,6 +270,21 @@ def test_setecho2
}
end

def test_tty_on_pty
pend "not supported" unless TTY_ENHANCED

helper {|_, s|
assert_predicate(s, :tty?)
assert_operator(s, :tty?, :any)
assert_operator(s, :tty?, nil)
assert_send([s, :tty?, :any, :any])
assert_send([s, :tty?, nil, :any])

assert_raise(TypeError) {s.tty?("any")}
assert_raise(ArgumentError) {s.tty?(:unknown)}
}
end

def test_getpass
run_pty("p IO.console.getpass('> ')") do |r, w|
assert_equal("> ", r.readpartial(10))
Expand Down Expand Up @@ -495,66 +538,65 @@ def run_pty(src, n = 1)
end
end

defined?(IO.console) and TestIO_Console.class_eval do
if IO.console
def test_get_winsize_console
s = IO.console.winsize
assert_kind_of(Array, s)
assert_equal(2, s.size)
assert_kind_of(Integer, s[0])
assert_kind_of(Integer, s[1])
end
defined?(IO.console) and IO.console and \
class TestIO_Console
def test_get_winsize_console
s = IO.console.winsize
assert_kind_of(Array, s)
assert_equal(2, s.size)
assert_kind_of(Integer, s[0])
assert_kind_of(Integer, s[1])
end

def test_set_winsize_console
set_winsize_setup
s = IO.console.winsize
assert_nothing_raised(TypeError) {IO.console.winsize = s}
bug = '[ruby-core:82741] [Bug #13888]'
begin
IO.console.winsize = [s[0], s[1]+1]
assert_equal([s[0], s[1]+1], IO.console.winsize, bug)
rescue Errno::EINVAL # Error if run on an actual console.
else
IO.console.winsize = s
assert_equal(s, IO.console.winsize, bug)
end
ensure
set_winsize_teardown
def test_set_winsize_console
set_winsize_setup
s = IO.console.winsize
assert_nothing_raised(TypeError) {IO.console.winsize = s}
bug = '[ruby-core:82741] [Bug #13888]'
begin
IO.console.winsize = [s[0], s[1]+1]
assert_equal([s[0], s[1]+1], IO.console.winsize, bug)
rescue Errno::EINVAL # Error if run on an actual console.
else
IO.console.winsize = s
assert_equal(s, IO.console.winsize, bug)
end
ensure
set_winsize_teardown
end

def test_close
IO.console.close
assert_kind_of(IO, IO.console)
assert_nothing_raised(IOError) {IO.console.fileno}
def test_close
IO.console.close
assert_kind_of(IO, IO.console)
assert_nothing_raised(IOError) {IO.console.fileno}

IO.console(:close)
assert(IO.console(:tty?))
ensure
IO.console(:close)
end
IO.console(:close)
assert(IO.console(:tty?))
ensure
IO.console(:close)
end

def test_console_kw
io = IO.console(:clone, freeze: true)
io.close
assert_kind_of(IO, io)
end
def test_console_kw
io = IO.console(:clone, freeze: true)
io.close
assert_kind_of(IO, io)
end

def test_sync
assert(IO.console.sync, "console should be unbuffered")
ensure
IO.console(:close)
end
def test_sync
assert(IO.console.sync, "console should be unbuffered")
ensure
IO.console(:close)
end

def test_getch_timeout
assert_nil(IO.console.getch(intr: true, time: 0.1, min: 0))
end
def test_getch_timeout
assert_nil(IO.console.getch(intr: true, time: 0.1, min: 0))
end

def test_ttyname
return unless IO.method_defined?(:ttyname)
ttyname = IO.console.ttyname
assert_not_nil(ttyname)
File.open(ttyname) {|f| assert_predicate(f, :tty?)}
end
def test_ttyname
return unless IO.method_defined?(:ttyname)
ttyname = IO.console.ttyname
assert_not_nil(ttyname)
File.open(ttyname) {|f| assert_predicate(f, :tty?)}
end

case
Expand All @@ -563,6 +605,8 @@ def test_ttyname
when !(rubyw = RbConfig::CONFIG["RUBYW_INSTALL_NAME"]).empty?
dir, base = File.split(EnvUtil.rubybin)
noctty = [File.join(dir, base.sub(RUBY_ENGINE, rubyw))]
else
rubyw = nil
end

if noctty
Expand Down Expand Up @@ -605,8 +649,8 @@ def test_noctty
end
end

defined?(IO.console) and IO.console and IO.console.respond_to?(:pressed?) and
TestIO_Console.class_eval do
defined?(IO.console) and IO.console and IO.console.respond_to?(:pressed?) and \
class TestIO_Console
def test_pressed_valid
assert_include([true, false], IO.console.pressed?("HOME"))
assert_include([true, false], IO.console.pressed?(:"HOME"))
Expand All @@ -620,7 +664,7 @@ def test_pressed_invalid
end
end

TestIO_Console.class_eval do
class TestIO_Console
def test_stringio_getch
assert_ruby_status %w"--disable=gems -rstringio -rio/console", %q{
abort unless StringIO.method_defined?(:getch)
Expand Down
Loading