From fdad3515018243edf0e7e8fa3a910d3774195ce3 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 10 Apr 2024 20:00:51 +0900 Subject: [PATCH] Add IO#ttyname that returns the tty name or nil --- ext/io/console/console.c | 61 ++++++++++++++++++++++++++++++ ext/io/console/extconf.rb | 2 + test/io/console/test_io_console.rb | 25 ++++++++++-- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/ext/io/console/console.c b/ext/io/console/console.c index 1fbf9bc..07ab10d 100644 --- a/ext/io/console/console.c +++ b/ext/io/console/console.c @@ -84,6 +84,11 @@ getattr(int fd, conmode *t) static ID id_getc, id_close; static ID id_gets, id_flush, id_chomp_bang; +#ifndef HAVE_RB_INTERNED_STR_CSTR +# define rb_str_to_interned_str(str) rb_str_freeze(str) +# define rb_interned_str_cstr(str) rb_str_freeze(rb_usascii_str_new_cstr(str)) +#endif + #if defined HAVE_RUBY_FIBER_SCHEDULER_H # include "ruby/fiber/scheduler.h" #elif defined HAVE_RB_SCHEDULER_TIMEOUT @@ -1818,6 +1823,61 @@ io_getpass(int argc, VALUE *argv, VALUE io) return str_chomp(str); } +#if defined(_WIN32) || defined(HAVE_TTYNAME_R) || defined(HAVE_TTYNAME) +/* + * call-seq: + * io.ttyname -> string or nil + * + * Returns name of associated terminal (tty) if +io+ is not a tty. + * Returns +nil+ otherwise. + */ +static VALUE +console_ttyname(VALUE io) +{ + int fd = rb_io_descriptor(io); + if (!isatty(fd)) return Qnil; +# if defined _WIN32 + return rb_usascii_str_new_lit("con"); +# elif defined HAVE_TTYNAME_R + { + char termname[1024], *tn = termname; + size_t size = sizeof(termname); + int e; + if (ttyname_r(fd, tn, size) == 0) + return rb_interned_str_cstr(tn); + if ((e = errno) == ERANGE) { + VALUE s = rb_str_new(0, size); + while (1) { + tn = RSTRING_PTR(s); + size = rb_str_capacity(s); + if (ttyname_r(fd, tn, size) == 0) { + return rb_str_to_interned_str(rb_str_resize(s, strlen(tn))); + } + if ((e = errno) != ERANGE) break; + if ((size *= 2) >= INT_MAX/2) break; + rb_str_resize(s, size); + } + } + rb_syserr_fail_str(e, rb_sprintf("ttyname_r(%d)", fd)); + UNREACHABLE_RETURN(Qnil); + } +# elif defined HAVE_TTYNAME + { + const char *tn = ttyname(fd); + if (!tn) { + int e = errno; + rb_syserr_fail_str(e, rb_sprintf("ttyname(%d)", fd)); + } + return rb_interned_str_cstr(tn); + } +# else +# error No ttyname function +# endif +} +#else +# define console_ttyname rb_f_notimplement +#endif + /* * IO console methods */ @@ -1885,6 +1945,7 @@ InitVM_console(void) rb_define_method(rb_cIO, "pressed?", console_key_pressed_p, 1); 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); rb_define_singleton_method(rb_cIO, "console", console_dev, -1); { /* :stopdoc: */ diff --git a/ext/io/console/extconf.rb b/ext/io/console/extconf.rb index 30de410..4ad7ed6 100644 --- a/ext/io/console/extconf.rb +++ b/ext/io/console/extconf.rb @@ -9,6 +9,7 @@ have_func("rb_syserr_new_str(0, Qnil)") or abort +have_func("rb_interned_str_cstr") have_func("rb_io_path") have_func("rb_io_descriptor") have_func("rb_io_get_write_io") @@ -51,6 +52,7 @@ elsif have_func("rb_scheduler_timeout") # 3.0 have_func("rb_io_wait") end + have_func("ttyname_r") or have_func("ttyname") 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 0a113eb..2bf3df6 100644 --- a/test/io/console/test_io_console.rb +++ b/test/io/console/test_io_console.rb @@ -441,6 +441,11 @@ def test_console_kw def test_sync assert_equal(["true"], run_pty("p IO.console.sync")) end + + def test_ttyname + return unless IO.method_defined?(:ttyname) + assert_equal(["true"], run_pty("p STDIN.ttyname == STDOUT.ttyname")) + end end private @@ -531,6 +536,13 @@ def test_sync 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 end end @@ -546,7 +558,7 @@ def test_getch_timeout if noctty require 'tempfile' NOCTTY = noctty - def test_noctty + def run_noctty(src) t = Tempfile.new("noctty_out") t.close t2 = Tempfile.new("noctty_run") @@ -557,7 +569,7 @@ def test_noctty '-e', 'STDOUT.reopen(f)', '-e', 'STDERR.reopen(f)', '-e', 'require "io/console"', - '-e', 'f.puts IO.console.inspect', + '-e', "f.puts (#{src}).inspect", '-e', 'f.flush', '-e', 'File.unlink(ARGV[1])', '-e', '}', @@ -568,11 +580,18 @@ def test_noctty sleep 0.1 end t.open - assert_equal("nil", t.gets(nil).chomp) + t.gets.lines(chomp: true) ensure t.close! if t and !t.closed? t2.close! end + + def test_noctty + assert_equal(["nil"], run_noctty("IO.console")) + if IO.method_defined?(:ttyname) + assert_equal(["nil"], run_noctty("STDIN.ttyname rescue $!")) + end + end end end