diff --git a/ext/io/console/console.c b/ext/io/console/console.c index bedfb27..ebd4c39 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -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 */ @@ -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: */ diff --git a/ext/io/console/extconf.rb b/ext/io/console/extconf.rb index 6f11793..95680dc 100644 --- a/ext/io/console/extconf.rb +++ b/ext/io/console/extconf.rb @@ -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}" @@ -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" } diff --git a/test/io/console/test_io_console.rb b/test/io/console/test_io_console.rb index c3f9c91..1ce1de0 100644 --- a/test/io/console/test_io_console.rb +++ b/test/io/console/test_io_console.rb @@ -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 @@ -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)) @@ -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 @@ -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 @@ -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")) @@ -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)