From 3b77c42aafddcb9bf220a6f6cce112fdd2396a8c Mon Sep 17 00:00:00 2001 From: "Yury V. Zaytsev" Date: Sun, 4 Oct 2026 06:41:43 +0000 Subject: [PATCH 1/3] lib/tty: attach redirected stdin to the controlling terminal MC reads the keyboard from stdin with the ncurses backend, and both backends save the initial terminal modes from stdin. When stdin is redirected (e.g. "mc < /dev/null" or "find ... | xargs mcedit", GNU xargs gives the command /dev/null as stdin): * the ncurses build busy-loops on EOF, or blocks on a pipe, and does not react to the keyboard at all; * the S-Lang build reads the keyboard from /dev/tty, but saves the "shell" terminal modes from stdin, which fails and leaves them zero-filled, so every external command is run on a terminal with speed 0 and no echo, signals or output post-processing. Before initializing the terminal, attach a redirected stdin to the controlling terminal, like "xargs -o" does (fall back to stderr like S-Lang does if there is no controlling terminal), and fail with a clear message only if there is no terminal at all. MC never reads data from stdin; a future implementation of reading the viewer data from stdin (#2370) will have to dup() the original stdin before this call. Assisted-By: Claude Opus 5.5 Signed-off-by: Yury V. Zaytsev --- lib/tty/tty.c | 40 ++++++++++++++++++++++++++++++++++++++++ lib/tty/tty.h | 1 + src/main.c | 9 +++++++++ 3 files changed, 50 insertions(+) diff --git a/lib/tty/tty.c b/lib/tty/tty.c index b28d196df7..3ab2622267 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 48c055daf5..8616817f50 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 a67270150e..0a80e9affe 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; From dc6be07c65949348094fdaedab9ac669c3565962 Mon Sep 17 00:00:00 2001 From: "Yury V. Zaytsev" Date: Sun, 4 Oct 2026 06:50:36 +0000 Subject: [PATCH 2/3] tests: lib/tty: add tests for tty_stdin_to_terminal() Run tty_stdin_to_terminal() in a new session on a fresh pseudo-terminal and check that a key typed on that terminal can be read from stdin when stdin is a terminal, when it is redirected and there is a controlling terminal, and when it is redirected and only stderr is a terminal, and that a failure is reported with stdin left untouched if there is no terminal at all. Assisted-By: Claude Opus 5.5 Signed-off-by: Yury V. Zaytsev --- tests/lib/tty.c | 174 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/tests/lib/tty.c b/tests/lib/tty.c index b81d76f842..5792a8bc9b 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); From bf203994e77bd29969f00099dde79ca94ace16a0 Mon Sep 17 00:00:00 2001 From: "Yury V. Zaytsev" Date: Sun, 4 Oct 2026 06:55:10 +0000 Subject: [PATCH 3/3] lib/tty: check the results of tcgetattr() and SLang_init_tty() tty_init() ignored the result of tcgetattr() in both backends: * ncurses: an uninitialized structure was applied to the terminal with tcsetattr(). Only change VINTR/VQUIT if the modes were read. * S-Lang: boot_mode stayed zero-filled and was applied to the terminal before running every external command (speed 0, no echo, no signals, no output post-processing). Fail early instead, before the terminal is touched, like for an unsupported screen size. Also fail if SLang_init_tty() does, instead of running on a terminal that S-Lang has not set up. Assisted-By: Claude Opus 5.5 Signed-off-by: Yury V. Zaytsev --- lib/tty/tty-ncurses.c | 15 +++++++++------ lib/tty/tty-slang.c | 15 +++++++++++++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/tty/tty-ncurses.c b/lib/tty/tty-ncurses.c index 23d2cced5f..fa77910005 100644 --- a/lib/tty/tty-ncurses.c +++ b/lib/tty/tty-ncurses.c @@ -243,12 +243,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 (); diff --git a/lib/tty/tty-slang.c b/lib/tty/tty-slang.c index 0b8e61e2f3..fc788b0be7 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 (fileno (stdin), &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;