Skip to content

lib/tty: add proper error handling for tcgetattr() - #5173

Open
mukhanov wants to merge 3 commits into
MidnightCommander:masterfrom
mukhanov:fix/tcgetattr-error-handling
Open

mukhanov wants to merge 3 commits into
MidnightCommander:masterfrom
mukhanov:fix/tcgetattr-error-handling

Conversation

@mukhanov

@mukhanov mukhanov commented Oct 3, 2026

Copy link
Copy Markdown
  • Check if stdin is a terminal before calling tcgetattr()
  • Verify return value of tcgetattr() and handle errors properly
  • Exit with clear error message when stdin is not a terminal
  • Prevents mc from hanging when run in non-interactive environments

This fixes the issue where mc would hang on startup when stdin is not
a terminal (e.g., when run in background or in certain terminal emulators
on macOS). The program now exits with a clear error message instead of
hanging or exhibiting undefined behavior.

@github-actions github-actions Bot added needs triage Needs triage by maintainers prio: medium Has the potential to affect progress labels Oct 3, 2026
@github-actions github-actions Bot added this to the Future Releases milestone Oct 3, 2026
@mc-worker

Copy link
Copy Markdown
Contributor

Treating !isatty (fileno (stdin)) as an error makes impossible an implementation of #2370 and #2629 in the future.

Btw, tty_init() in the lib/tty/tty-ncurses.c calls tcgetattr() without return value checking too.

@mukhanov
mukhanov force-pushed the fix/tcgetattr-error-handling branch from a1ffbc2 to d08e61a Compare October 3, 2026 22:08
@mukhanov

mukhanov commented Oct 3, 2026

Copy link
Copy Markdown
Author

Thank you for the feedback!

I've updated the fix to:

  1. Only treat !isatty() as an error for MC_RUN_FULL mode
  2. For MC_RUN_VIEWER and MC_RUN_EDITOR modes, tcgetattr() failures are handled gracefully by initializing the mode structure with default values
  3. Also fixed the same issue in lib/tty/tty-ncurses.c

This approach:

The changes ensure that viewer/editor modes can work with pipes while full mc mode still requires a proper terminal.

@zyv zyv added area: tty Interaction with the terminal, screen libraries and removed needs triage Needs triage by maintainers labels Oct 4, 2026
@zyv

zyv commented Oct 4, 2026

Copy link
Copy Markdown
Member

@mukhanov - thanks for your submission. I've screened it through Opus 5.5, and it has identified a number of issues and suggests a different approach. Would you be up to reviewing and testing the suggested alternative? I can submit the results later this coming week.

@mukhanov

mukhanov commented Oct 4, 2026

Copy link
Copy Markdown
Author

Hi @zyv,

Yes, I'd be happy to review and test the alternative approach suggested by Opus 5.5.

This is indeed a rather annoying bug - mc hanging on startup when stdin is not a terminal is a real problem that needs a proper fix. I'm particularly interested in seeing how Opus 5.5 handles the error cases differently from my initial approach.

Looking forward to reviewing the results when you have them ready.

Best regards

zyv added 3 commits October 10, 2026 17:59
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 (MidnightCommander#2370) will have to dup() the original stdin before this call.

Assisted-By: Claude Opus 5.5
Signed-off-by: Yury V. Zaytsev <yury@shurup.com>
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 <yury@shurup.com>
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 <yury@shurup.com>
@zyv
zyv force-pushed the fix/tcgetattr-error-handling branch from 94f0396 to bf20399 Compare October 10, 2026 15:59
@zyv

zyv commented Oct 10, 2026

Copy link
Copy Markdown
Member

Okay, I have finally managed to process the alternative patch series generated with Opus 5.5, which do make sense to me. It includes a test that passes with the suggested changes and fails with your approach, and I think it addresses both points by @mc-worker. I've added the edited LLM review summary below for the details. Appreciate it if you could review and test it @mukhanov and report back.


The underlying problem is real, but the PR addresses it at the wrong level, and every run test reproduces at least one damaging behaviour:

  • It breaks a working use case on the default S-Lang backend. mc < /dev/null and ... | xargs mc work on master and now fail (F1).
  • It leaves the user's terminal without echo or line editing when the new error exits are taken on ncurses (F2).
  • It does not fix the hang it describes for mcview / mcedit / mcdiff on ncurses. They still spin at 100 % CPU on EOF or ignore the keyboard on a pipe (F3).
  • It codifies the zero-filled boot_mode as "default values". That mode is applied to the real terminal before every external command: speed 0, no ISIG (Ctrl-C cannot stop the command), no echo (F4, pre-existing).

Right fix: mc never reads data from stdin, but it reads the keyboard from stdin with ncurses, and both backends save the initial terminal modes from stdin. So the correct fix is to attach a redirected stdin to the controlling terminal once, in main(), before any terminal set-up, as xargs -o does 1. Fall back to stderr as S-Lang does 2, and fail with a clear message only when there is no terminal at all. This:


# Severity Finding
F1 High FULL mode now refuses to start with redirected stdin on S-Lang (the default backend), although S-Lang reads keys from /dev/tty and this worked
F2 High Zero-filled boot_mode is applied to the real terminal before every external command (speed 0, -isig -icanon -echo -opost). Pre-existing, but the PR rewrites this code and documents it as safe. Run test (stty -a from user menu).
F3 High The hang the PR targets remains for viewer, editor and diff viewer on ncurses (100 % CPU on EOF, keyboard ignored on a pipe). The "stdin may be a pipe, e.g. cat file | mcview" premise is not a feature mc has
F4 Medium ncurses: both new exit() paths run after initscr() without endwin(), leaving the shell with -echo -icanon -onlcr
F5 Low After a failed tcgetattr() the zero-filled struct is still written with tcsetattr() (ncurses). The S-Lang new_mode check tests the wrong call: SLang_init_tty()'s result is ignored
F6 Low exit() from inside tty_init() bypasses main()'s startup cleanup and error format ("Failed to run: ...")
F7 Low Same 15-line block pasted 3 times; two new translatable strings in a non-standard format; redundant #include <string.h>; errno used without <errno.h>

Footnotes

  1. GNU findutils 4.9.0 xargs --help output (-o, --open-tty) and local run (echo x | xargs sh -c 'readlink /proc/$$/fd/0' → /dev/null). Local tool output, Ubuntu 24.04 container. ↩

  2. [8] S-Lang 2.3.3 source, src/slutty.c (SLang_init_tty(), lines 276-321). Ubuntu archive upstream tarball. ↩

@zyv

zyv commented Oct 10, 2026

Copy link
Copy Markdown
Member

P.S. I can look into the Solaris issue when everybody is satisfied with the approach in principle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: tty Interaction with the terminal, screen libraries prio: medium Has the potential to affect progress

Development

Successfully merging this pull request may close these issues.

3 participants