diff --git a/lib/tty/key.c b/lib/tty/key.c index 0c679f602..f21be04d0 100644 --- a/lib/tty/key.c +++ b/lib/tty/key.c @@ -40,12 +40,12 @@ #include #include #include +#include #ifdef HAVE_SYS_SELECT_H #include #else #include #include -#include #endif #include "lib/global.h" @@ -952,7 +952,7 @@ get_modifier (void) int mod_status; int shift_ext_status; - if (devctl (fileno (stdin), DCMD_CHR_LINESTATUS, &mod_status, sizeof (mod_status), NULL) + if (devctl (STDIN_FILENO, DCMD_CHR_LINESTATUS, &mod_status, sizeof (mod_status), NULL) == -1) return 0; diff --git a/lib/tty/keyxdef.c b/lib/tty/keyxdef.c index 33369fbef..e581eface 100644 --- a/lib/tty/keyxdef.c +++ b/lib/tty/keyxdef.c @@ -413,7 +413,7 @@ load_qnx_key_defines (void) int idx; int term_setup_ok; - __setupterm (NULL, fileno (stdout), &term_setup_ok); + __setupterm (NULL, STDOUT_FILENO, &term_setup_ok); if (term_setup_ok != 1) return; diff --git a/lib/tty/tty-ncurses.c b/lib/tty/tty-ncurses.c index 23d2cced5..85dbc9290 100644 --- a/lib/tty/tty-ncurses.c +++ b/lib/tty/tty-ncurses.c @@ -35,6 +35,7 @@ #include #include #include +#include #ifdef HAVE_SYS_IOCTL_H #include #endif @@ -243,12 +244,15 @@ tty_init (gboolean mouse_enable, gboolean is_xterm) ESCDELAY = 200; #endif - tcgetattr (STDIN_FILENO, &mode); - // use Ctrl-g to generate SIGINT - mode.c_cc[VINTR] = CTRL ('g'); // ^g - // disable SIGQUIT to allow use Ctrl-\ key - mode.c_cc[VQUIT] = NULL_VALUE; - tcsetattr (STDIN_FILENO, TCSANOW, &mode); + // don't apply an uninitialized structure if the terminal modes cannot be read + if (tcgetattr (STDIN_FILENO, &mode) == 0) + { + // use Ctrl-g to generate SIGINT + mode.c_cc[VINTR] = CTRL ('g'); // ^g + // disable SIGQUIT to allow use Ctrl-\ key + mode.c_cc[VQUIT] = NULL_VALUE; + tcsetattr (STDIN_FILENO, TCSANOW, &mode); + } // curses remembers the "in-program" modes after this call def_prog_mode (); @@ -320,7 +324,7 @@ tty_change_screen_size (void) #endif // Ioctl on the STDIN_FILENO - ioctl (fileno (stdout), TIOCGWINSZ, &winsz); + ioctl (STDOUT_FILENO, TIOCGWINSZ, &winsz); if (winsz.ws_col != 0 && winsz.ws_row != 0) { #if defined(NCURSES_VERSION) && defined(HAVE_RESIZETERM) diff --git a/lib/tty/tty-slang.c b/lib/tty/tty-slang.c index 0b8e61e2f..b580d8454 100644 --- a/lib/tty/tty-slang.c +++ b/lib/tty/tty-slang.c @@ -31,6 +31,7 @@ #include +#include #include // MB_LEN_MAX #include #include @@ -249,9 +250,19 @@ tty_init (gboolean mouse_enable, gboolean is_xterm) exit (EXIT_FAILURE); } - tcgetattr (fileno (stdin), &boot_mode); + // the terminal modes are restored from boot_mode before running external commands + if (tcgetattr (STDIN_FILENO, &boot_mode) != 0) + { + fprintf (stderr, _ ("Cannot get terminal settings: %s\n"), unix_error_string (errno)); + exit (EXIT_FAILURE); + } + // 255 = ignore abort char; XCTRL('g') for abort char = ^g - SLang_init_tty (XCTRL ('g'), 1, 0); + if (SLang_init_tty (XCTRL ('g'), 1, 0) != 0) + { + fprintf (stderr, _ ("Cannot initialize the terminal: %s\n"), unix_error_string (errno)); + exit (EXIT_FAILURE); + } if (mc_global.tty.ugly_line_drawing) SLtt_Has_Alt_Charset = 0; diff --git a/lib/tty/tty.c b/lib/tty/tty.c index b28d196df..3ab262226 100644 --- a/lib/tty/tty.c +++ b/lib/tty/tty.c @@ -31,6 +31,7 @@ #include #include +#include // open() #include #include #include @@ -143,6 +144,45 @@ tty_check_xterm_compat (const gboolean force_xterm) return FALSE; } + +/* --------------------------------------------------------------------------------------------- */ +/** + * Make sure that the standard input is a terminal. + * + * MC does not read data from stdin, but it reads the keyboard from there: the ncurses backend + * and the key reader use stdin, and both backends save the initial terminal modes from it. + * If stdin is redirected (e.g. "mc < /dev/null" or "find ... | xargs mcedit"), attach it to the + * controlling terminal, like "xargs -o" does. Like S-Lang, fall back to stderr if there is + * no controlling terminal. + * + * @return TRUE if stdin is a terminal now, FALSE otherwise + */ +gboolean +tty_stdin_to_terminal (void) +{ + int fd; + + if (isatty (STDIN_FILENO)) + return TRUE; + + fd = open ("/dev/tty", O_RDWR); + if (fd == -1 && isatty (STDERR_FILENO)) + fd = dup (STDERR_FILENO); + if (fd == -1) + return FALSE; + + if (fd != STDIN_FILENO) + { + const int ret = dup2 (fd, STDIN_FILENO); + + close (fd); + if (ret == -1) + return FALSE; + } + + return isatty (STDIN_FILENO) != 0; +} + /* --------------------------------------------------------------------------------------------- */ extern void diff --git a/lib/tty/tty.h b/lib/tty/tty.h index 48c055daf..8616817f5 100644 --- a/lib/tty/tty.h +++ b/lib/tty/tty.h @@ -103,6 +103,7 @@ extern void tty_beep (void); /* {{{ Input }}} */ extern gboolean tty_check_xterm_compat (gboolean force_xterm); +extern gboolean tty_stdin_to_terminal (void); extern void tty_init (gboolean mouse_enable, gboolean is_xterm); extern void tty_shutdown (void); diff --git a/src/main.c b/src/main.c index a67270150..0a80e9aff 100644 --- a/src/main.c +++ b/src/main.c @@ -273,6 +273,15 @@ main (int argc, char *argv[]) goto startup_exit_ok; } + if (!tty_stdin_to_terminal ()) + { + mc_propagate_error (&mcerror, 0, "%s", + _ ("Standard input is not a terminal and there is no controlling " + "terminal to read the keyboard from")); + mc_event_deinit (NULL); + goto startup_exit_falure; + } + if (!events_init (&mcerror)) goto startup_exit_falure; diff --git a/tests/lib/tty.c b/tests/lib/tty.c index b81d76f84..5792a8bc9 100644 --- a/tests/lib/tty.c +++ b/tests/lib/tty.c @@ -24,7 +24,15 @@ #include "tests/mctest.h" +#include #include +#include +#include +#include +#ifdef HAVE_SYS_IOCTL_H +#include +#endif +#include #include "lib/strutil.h" #include "lib/util.h" @@ -109,6 +117,168 @@ END_TEST /* --------------------------------------------------------------------------------------------- */ +/* Open a new pseudo-terminal and return the descriptor of its slave side. If @controlling is + TRUE, the calling process must be a session leader, and the slave becomes its controlling + terminal. The master descriptor is stored in @master. */ +static int +open_pty_slave (int *master, gboolean controlling) +{ + int slave; + + *master = posix_openpt (O_RDWR | O_NOCTTY); + if (*master == -1 || grantpt (*master) != 0 || unlockpt (*master) != 0) + return -1; + + slave = open (ptsname (*master), O_RDWR | (controlling ? 0 : O_NOCTTY)); +#ifdef TIOCSCTTY + if (slave != -1 && controlling && ioctl (slave, TIOCSCTTY, 0) == -1) + return -1; +#endif + + return slave; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +same_file (int fd1, int fd2) +{ + struct stat st1, st2; + + return fstat (fd1, &st1) == 0 && fstat (fd2, &st2) == 0 && st1.st_dev == st2.st_dev + && st1.st_ino == st2.st_ino; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* Check that a key typed on the terminal with the @master side can be read from stdin */ +static gboolean +key_reaches_stdin (int master) +{ + char buf[8] = ""; + + alarm (5); // don't hang the test if stdin is not that terminal + return write (master, "k\n", 2) == 2 && read (STDIN_FILENO, buf, sizeof (buf)) == 2 + && buf[0] == 'k'; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* Session setups for tty_stdin_to_terminal() tests */ +typedef enum +{ + STDIN_IS_TTY, // stdin is a terminal, no controlling terminal + STDIN_REDIRECTED, // stdin is /dev/null, there is a controlling terminal + STDIN_REDIRECTED_NO_CTTY_STDERR_TTY, // stdin is /dev/null, no ctty, stderr is a terminal + NO_TERMINAL_AT_ALL, // stdin and stderr are /dev/null, no controlling terminal +} stdin_setup_t; + +/* Run tty_stdin_to_terminal() in a new session prepared according to @setup and return + 0 if the result is as expected, a positive code of the failed check otherwise. */ +static int +run_stdin_to_terminal (stdin_setup_t setup) +{ + pid_t pid; + int status; + + pid = fork (); + if (pid == 0) + { + int master, slave, devnull, saved_stdin; + gboolean ret; + + if (setsid () == -1) + _exit (10); + + slave = open_pty_slave (&master, setup == STDIN_REDIRECTED); + devnull = open ("/dev/null", O_RDWR); + if (slave == -1 || devnull == -1) + _exit (11); + + dup2 (setup == STDIN_IS_TTY ? slave : devnull, STDIN_FILENO); + dup2 (setup == STDIN_REDIRECTED_NO_CTTY_STDERR_TTY ? slave : devnull, STDERR_FILENO); + saved_stdin = dup (STDIN_FILENO); + + ret = tty_stdin_to_terminal (); + + if (setup == NO_TERMINAL_AT_ALL) + // must fail and leave stdin untouched + _exit (ret ? 1 : (same_file (STDIN_FILENO, saved_stdin) ? 0 : 2)); + + if (!ret) + _exit (3); + if (!isatty (STDIN_FILENO)) + _exit (4); + // stdin must be the pty: the original one, /dev/tty or stderr + if (!key_reaches_stdin (master)) + _exit (5); + _exit (0); + } + + if (pid == -1 || waitpid (pid, &status, 0) != pid || !WIFEXITED (status)) + return 100; + + return WEXITSTATUS (status); +} + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_tty_stdin_to_terminal_stdin_is_tty) +{ + // given: stdin is a terminal + + // when + const int actual_result = run_stdin_to_terminal (STDIN_IS_TTY); + + // then: stdin is left as is + ck_assert_int_eq (actual_result, 0); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_tty_stdin_to_terminal_redirected) +{ + // given: "mc < /dev/null" run from a terminal + + // when + const int actual_result = run_stdin_to_terminal (STDIN_REDIRECTED); + + // then: stdin is the controlling terminal + ck_assert_int_eq (actual_result, 0); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_tty_stdin_to_terminal_stderr_fallback) +{ + // given: stdin is redirected, no controlling terminal, stderr is a terminal + + // when + const int actual_result = run_stdin_to_terminal (STDIN_REDIRECTED_NO_CTTY_STDERR_TTY); + + // then: stdin is the terminal of stderr + ck_assert_int_eq (actual_result, 0); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_tty_stdin_to_terminal_no_terminal) +{ + // given: no terminal at all + + // when + const int actual_result = run_stdin_to_terminal (NO_TERMINAL_AT_ALL); + + // then: failure is reported and stdin is left as is + ck_assert_int_eq (actual_result, 0); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + int main (void) { @@ -121,6 +291,10 @@ main (void) tcase_add_test (tc_core, test_tty_check_term_set_empty); tcase_add_test (tc_core, test_tty_check_term_non_xterm); tcase_add_test (tc_core, test_tty_check_term_xterm_like); + tcase_add_test (tc_core, test_tty_stdin_to_terminal_stdin_is_tty); + tcase_add_test (tc_core, test_tty_stdin_to_terminal_redirected); + tcase_add_test (tc_core, test_tty_stdin_to_terminal_stderr_fallback); + tcase_add_test (tc_core, test_tty_stdin_to_terminal_no_terminal); // *********************************** return mctest_run_all (tc_core);