Search code examples
c++sshttypty

Double echo when running commands under a pty


I'm writing a program to create a pty, then fork and execute an ssh command with the slave side of the pty as its stdin. The full source code is here.

using namespace std;
#include <iostream>
#include <unistd.h>
#include <fcntl.h>

int main() {

    int fd = posix_openpt(O_RDWR);
    grantpt(fd);
    unlockpt(fd);

    pid_t pid = fork();

    if (pid == 0) { //slave

        freopen(ptsname(fd), "r", stdin);

        execlp("ssh", "ssh", "user@192.168.11.40", NULL);

    } else { //master

        FILE *f = fdopen(fd, "w");
        string buf;

        while (true) {

            getline(cin, buf);

            if (!cin) {
                break;
            }

            fprintf(f, "%s\n", buf.c_str());

        }

    }

}

After executing this program and inputting just echo hello (and a newline), the child command re-sends my input before its own output, thus duplicating my input line:

~ $ echo hello
echo hello #duplication
hello

~ $ 

I think this is due to the fact that a pty behaves almost the same as a normal terminal. If I add freopen("log.txt", "w", stdout);" and input the same command, I get just

echo hello #This is printed because I typed it.

and the contents of log.txt is this:

~ $ echo hello #I think this is printed because a pty simulates input.
hello                             

~ $

How can I avoid the duplication?


Is that realizable?

I know it is somehow realizable, but don't know how to. In fact, the rlwrap command behaves the same as my program, except that it doesn't have any duplication:

~/somedir $ rlwrap ssh user@192.168.11.40
~ $ echo hello
hello

~ $

I'm reading the source code of rlwrap now, but haven't yet understood its implementation.


Supplement

As suggested in this question (To me, not the answer but the OP was helpful.), unsetting the ECHO terminal flag disables the double echoing. In my case, adding this snippets to the slave block solved the problem.

termios terminal_attribute;
int fd_slave = fileno(fopen(ptsname(fd_master), "r"));
tcgetattr(fd_slave, &terminal_attribute);
terminal_attribute.c_lflag &= ~ECHO;
tcsetattr(fd_slave, TCSANOW, &terminal_attribute);

It should be noted that this is not what rlwrap does. As far as I tested rlwrap <command> never duplicates its input line for any <command> However, my program echoes twice for some <command>s. For example,

~ $ echo hello
hello #no duplication

~ $ /usr/bin/wolfram
Mathematica 12.0.1 Kernel for Linux ARM (32-bit)
Copyright 1988-2019 Wolfram Research, Inc.

In[1]:= 3 + 4                                                                   
        3 + 4 #duplication (my program makes this while `rlwrap` doesn't)

Out[1]= 7

In[2]:=    

Is this because the <command> (ssh when I run wolfram remotely) re-enables echoing? Anyway, I should keep reading the source code of rlwrap.


Solution

  • As you already observed, after the child has called exec() the terminal flags of the slave side are not under your control anymore, and the child may (and often will) re-enable echo. This means that is is not of much use to change the terminal flags in the child before calling exec.

    Both rlwrap and rlfe solve the problem in their own (different) ways:

    Whatever approach you use, you have to know whether your input has been (in rlfes case) or will be (in rlwraps case) echoed back. rlwrap, at least, does this by not closing the pty's slave end in the parent process, and then watching its terminal settings (in this case, the ECHO bit in its c_lflag) to know whether the slave will echo or not.

    All this is rather cumbersome, of course. The rlfe approach is probably easier, as it doesn't require the use of the readline library, and you could simply strcmp() the received output with the input you just sent (which will only go wrong in the improbable case of a cat command that disables echo on its input)