Skip to content

[textinput] Keep the prompt on the terminal when stdout is redirected - #22948

Merged
guitargeek merged 3 commits into
root-project:masterfrom
guitargeek:issue-7626
Jul 29, 2026
Merged

[textinput] Keep the prompt on the terminal when stdout is redirected#22948
guitargeek merged 3 commits into
root-project:masterfrom
guitargeek:issue-7626

Conversation

@guitargeek

@guitargeek guitargeek commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

The .> file meta command redirects stdout to a file by dup2-ing fd 1, and the redirection persists across prompts. TerminalDisplayUnix, however, decided only once at construction where to write the prompt and line-editing output: it switched to /dev/tty only if stdout was already not a tty at startup. In an interactive session stdout is a tty at startup, so it kept writing to fd 1. Once .> file redirected fd 1 onto the file, the prompt, the echo of typed characters and the cursor movements all went into the file, leaving the command line invisible and unusable (arrow keys leaked raw escape sequences).

Keep a handle on the controlling terminal (/dev/tty) whenever stdin is a tty, and make WriteRawString() fall back to it when stdout is not (or no longer) connected to a terminal. As long as stdout is a terminal the prompt still goes to stdout exactly as before, so nothing changes for the common interactive case; only once stdout is redirected (e.g. by .> file) does the prompt go to the controlling terminal, keeping the command line usable while just the executed code's output follows the redirection.

This also guards the open("/dev/tty") result: on failure we keep writing to stdout instead of leaving the descriptor at -1.

Closes #7626

🤖 Done with the help of AI.

A regression test is added in a separate commit.

@dpiparo dpiparo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impressive.
Before merging though, I think we could provide a test, to never have the problem again.
Now, could the tool we use for testing TTabCom be used for that? https://github.com/root-project/root/tree/master/roottest/root/rint
Basically what happens is that we have in ascii files the input we want emulate as typed by the user at the prompt, and with reference files we cross-check what Rint should produce. In this case, another minimal comparison, a diff really, should be performed on the piped output.
Would it make sense

The `.> file` meta command redirects stdout to a file by dup2-ing fd 1, and
the redirection persists across prompts. This left the interactive command
line unusable, because textinput decided where to write the prompt/line
editing and whether to configure the terminal based on stdout, which is no
longer the terminal after the redirection:

* TerminalDisplayUnix wrote the prompt, the echo of typed characters and the
  cursor movements to stdout, so they ended up in the file instead of on the
  terminal, leaving the command line invisible.
* TerminalConfigUnix::IsInteractive() required stdout to be a tty, so once
  stdout was redirected the terminal was no longer switched to raw mode. The
  terminal stayed in cooked mode, so e.g. the up arrow was echoed verbatim as
  "^[[A" instead of recalling the previous command.

Base both decisions on the terminal we actually read from and configure,
i.e. stdin (never touched by ".> file"):

* Keep a handle on the controlling terminal (/dev/tty) whenever stdin is a
  tty, and make WriteRawString() fall back to it when stdout is not (or no
  longer) connected to a terminal. As long as stdout is a terminal the prompt
  still goes to stdout exactly as before, so nothing changes for the common
  interactive case; only once stdout is redirected does the prompt go to the
  controlling terminal, while just the executed code's output follows the
  redirection.
* Make IsInteractive() depend on stdin (fFD) instead of stdout, so the
  terminal keeps being switched to raw mode and line editing keeps working.

Closes root-project#7626

🤖 Done with the help of AI.
Drive ROOT through a pseudo terminal (like driveTabCom.py) so it believes it
talks to an interactive terminal; pty.fork() also makes that pty the
controlling terminal, so /dev/tty resolves to it even on headless machines.

The emulated input redirects stdout to a file, runs two commands, recalls the
first one with the up arrow and re-executes it, then un-redirects and quits.
The driver prints, for comparison against the reference:
 * the content of the redirected file: it must contain only the output of the
   executed code, including the line produced by the recalled command. Before
   the fix the prompt and the terminal escape sequences leaked into the file.
 * whether the up arrow was echoed verbatim to the terminal: this happens when
   the terminal is stuck in cooked mode while stdout is redirected. Before the
   fix it was, so the arrow was echoed instead of recalling a command.

To keep the second check reliable, each emulated line is sent only once ROOT
has displayed the next prompt, i.e. once it has switched the terminal to raw
mode and is waiting for input. Typing while ROOT is still starting up or busy
would let the line discipline echo the keystrokes in cooked mode and produce a
false positive.

Both regressions were verified to make the test fail.

🤖 Done with the help of AI.
After the previous commit fOutputID is never assigned and stays equal to
STDOUT_FILENO for the whole lifetime of the object: the prompt output target
is now chosen dynamically in WriteRawString() (stdout, or the controlling
terminal when stdout is redirected). Drop the member and use STDOUT_FILENO
directly. This also removes the now-dead destructor branch that closed
fOutputID and the always-true condition in WriteRawString(). No behavior
change.

🤖 Done with the help of AI.
@guitargeek

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Indeed, it's good to implement a unit test, which I added in an additional commit to this PR.

@dpiparo

dpiparo commented Jul 29, 2026

Copy link
Copy Markdown
Member

thanks! As far as I am concerned, if all tests pass, this PR is good to be merged.

@guitargeek
guitargeek merged commit 793656d into root-project:master Jul 29, 2026
29 of 31 checks passed
@guitargeek
guitargeek deleted the issue-7626 branch July 29, 2026 11:54
@hahnjo

hahnjo commented Aug 6, 2026

Copy link
Copy Markdown
Member

FWIW commit ec7998a breaks roottest-root-rint-TabCom for me on AlmaLinux 8 because it's now printing the root [0] prompts that are not in the reference output. It's unclear why only I am affected, and the CI was green - any ideas?

@guitargeek

Copy link
Copy Markdown
Contributor Author

I see that failure too, when I run the tests locally on a computer that I don't ssh into. I guess it depends on whether the ctest process inherits a controlling terminal. Investigating.

@hahnjo

hahnjo commented Aug 6, 2026

Copy link
Copy Markdown
Member

Ah yes, I'm in SSH but inside tmux so some details behave a bit differently...

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

issue with the /dev/stdout redirection into a file

3 participants