LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Programming (http://www.linuxquestions.org/questions/programming-9/)
-   -   Theory of Terminal Emulators (http://www.linuxquestions.org/questions/programming-9/theory-of-terminal-emulators-900619/)

damix 09-01-2011 06:21 PM

Theory of Terminal Emulators
 
Hello everybody, I'm not a very experienced *NIX programmer and just yesterday I decided to explore a little more in detail the relationship between shells and terminal emulators. I had some difficulties in finding good theoretical information on this topic, so I decided to try to figure out how an application (the TE) could interact bidirectionally with another application (the shell). I downloaded the code of rxvt and I skimmed very fast through what I thought would be the relevant sections (I was mainly interested in the communication part, not in the details of different terminal standards like VT100 and VT220). After studying the code and also reading some other stuff about UNIX system calls, I came up with this simple program that seems to be working (C++, I'm a C++ guy). It is basically a console application that hosts /bin/bash, and acts as a filter between it and the user. To me it looks like a terminal emulation window without window. I used pipe(), fork(), execlp(), kill(), select(), read(), write(), dup2(). Now, I'm wondering, is the inter-process communication handled like this in the famous TEs like xterm, GNOME Terminal and Konsole, or they are based on something radically different? I wrote this code because I was not able to find a good didactical example. I hope it is someway useful. If you find it useful you can let me know :-) I would be quite happy about that.

Bye, have a nice day and happy coding.

Dario

Code:

// C++ includes
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <csignal>
#include <cassert>
#include <vector>

// UNIX includes
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>

int main(int argc, char *argv[])
{
        // This pipe is written by the TE and read by
        // the executed shell as STDIN.
        int pipeTEtoSH[2];
       
        // This pipe is written by the executed shell
        // as STDOUT and read by the TE.
        int pipeSHtoTE[2];
       
        // This pipe is written by the executed shell
        // as STDERR and read by the TE.
        int pipeSHtoTEerr[2];
       
        // Instantiation of the 3 pipes.
        int r1 = pipe(pipeTEtoSH);
        int r2 = pipe(pipeSHtoTE);
        int r3 = pipe(pipeSHtoTEerr);
       
        // Check that all the pipes were successfully
        // instantiated.
        if (r1 == -1 || r2 == -1 || r3 == -1)
        {
                std::cout << "Can not pipe." << std::endl;
                return -1;
        }
       
        // The shell will be run as a separate process.
        int pid = fork();
       
        // Check that a child process was created.
        if (pid == -1)
        {
                std::cout << "Can not fork." << std::endl;
                return -1;
        }
       
        // From now on the same source code will be
        // shared by two distinct processes; in the
        // parent process fork() returns the PID of
        // the child, while in the child process
        // it returns 0.
       
        if (pid != 0)
        {
                // The code in this if will be executed by the parent.
                // This is the TE logic.
               
                // TE receives data from the shell one char at a time.
                char c;
               
                // TE sends entire commands, possibly containing
                // whitespaces, to the shell.
                std::string cmd;
               
                do
                {
                        // Terminal emulator prompt.
                        std::cout << " " << std::flush;
                        std::getline(std::cin, cmd);
                       
                        if (cmd == "exit")
                        {
                                // We don't want to execute exit on the shell,
                                // because this would terminate the child
                                // process while the parent process, the TE,
                                // would keep running. Instead, we display an
                                // error message.
                               
                                std::cout << "Emulated terminal - use 'stop' instead." << std::endl;
                        }
                        else if (cmd == "info")
                        {
                                // This is a command implemented by the TE; it
                                // simply prints an informative message about
                                // the terminal emulator.
                               
                                std::cout << "Damix's Terminal Emulator, 2011." << std::endl;
                        }
                        else if (cmd != "stop")
                        {
                                // Any other command different from stop is
                                // sent to the shell using the appropriate
                                // pipe. The command is terminated by a newline.
                                write(pipeTEtoSH[1], cmd.c_str(), cmd.size());
                                write(pipeTEtoSH[1], "\n", 1);
                               
                                // Data structures needed for select() and
                                // non-blocking I/O.
                                fd_set rfd;
                                fd_set wfd;
                                fd_set efd;
                                timeval tv;

                                do {
                                        // Wait for data available on either the
                                        // STDOUT pipe or the STDERR pipe.
                                        FD_ZERO(&rfd);
                                        FD_ZERO(&wfd);
                                        FD_ZERO(&efd);
                                        FD_SET(pipeSHtoTE[0], &rfd);
                                        FD_SET(pipeSHtoTEerr[0], &rfd);
                                        tv.tv_sec = 0;
                                        tv.tv_usec = 100000;
                                        select(100, &rfd, &wfd, &efd, &tv);
                                       
                                        // Check for data on the STDOUT pipe; the
                                        // executed console will write most of the
                                        // output on this pipe.
                                        if (FD_ISSET(pipeSHtoTE[0], &rfd))
                                        {
                                                read(pipeSHtoTE[0], &c, 1);
                                                std::cout << c << std::flush;
                                        }
                                       
                                        // Check for data on the STDERR pipe; the
                                        // executed console will write error messages
                                        // on this pipe (for instance the error message
                                        // that results from typing an unexisting command.
                                        // Just for display purposes, the STDERR messages
                                        // will be shown in upper case.
                                        if (FD_ISSET(pipeSHtoTEerr[0], &rfd))
                                        {
                                                read(pipeSHtoTEerr[0], &c, 1);
                                                std::cout << (char)toupper(c) << std::flush;
                                        }
                                       
                                        // If no data is detected for more than 1/10 of a
                                        // second, reading is aborted and the TE prompts
                                        // the user again. The timeout is specified by
                                        // the tv.tv_sec and tv.tv_usec.
                                } while (FD_ISSET(pipeSHtoTE[0], &rfd) || FD_ISSET(pipeSHtoTEerr[0], &rfd));
                        }
                       
                        // Command stop is interpreted by the TE and
                        // results in the TE stopping prompting the
                        // user and proceed to kill the shell and
                        // itself.
                } while (cmd != "stop");

                // Parent TE process kills the child shell.
                kill(pid, SIGKILL);
        }
        else
        {
                // The code in this else will be executed by the child.
                // This is the shell logic.
               
                // Redirect STDIN.
                assert(dup2(pipeTEtoSH[0], 0) == 0);
               
                // Redirect STDOUT.
                assert(dup2(pipeSHtoTE[1], 1) == 1);
               
                // Redirect STDERR.
                assert(dup2(pipeSHtoTEerr[1], 2) == 2);
               
                // The executed shell is bash.
                execlp("bash", "/bin/bash", NULL);
        }
       
        // Everything went fine.
        return 0;
}


ta0kira 09-01-2011 06:52 PM

Sessions, process groups, and the controlling process group are very important to Unix terminals. Sending signals from the terminal is also important (e.g. Ctrl+C -> SIGINT), which relates to the process group controlling the terminal.
Kevin Barry

damix 09-02-2011 07:58 PM

ta0kira
 
Hello ta0kira, thanks for your reply :-)

I did some more research and it looks like the standard way to achieve this bidirectional communication is with forkpty() or equivalently with a combination of openpty(), fork() and login_tty(). There are still a few things I don't understand, for instance, who is responsible for printing the prompt? Is it the shell or the terminal emulator? I also downloaded the sources of ROTE from http://rote.sourceforge.net/ and it looks like a nice start for those interested in this topic. However, a more theoretical description of these concepts would be great.

ta0kira 09-02-2011 08:38 PM

For the most part the shell is just an interactive line-based program like you might write in C. Normally it's the session leader, which essentially means it's the "supreme owner" of the terminal and it gets to determine which process group can read from the terminal and change the terminal's settings. When the shell needs keyboard input it prints the prompt to the pty (after making sure input is canonical and echoing is on) and reads from the pty, which the terminal program writes to based on input from the UI. When a process writes to the pty the terminal program reads it and updates the display as necessary. I'm not exactly sure how the terminal program knows when the termios settings change since I've never written a terminal emulator, but I'm assuming it's either sent a signal or it reads the settings when they're relevant.
Kevin Barry

damix 09-05-2011 06:04 AM

An example with forkpty
 
Hi everybody, I wrote another little example program. This time I used forkpty(). You can compile it with the following command

g++ -lutil forkptyte.cc

In this approach the shell itself is printing the prompt; the terminal emulator uses a single master file descriptor, returned by forkpty(), for both writing and reading to/from the shell. This apparently means that when it writes to the master at line 90:

90: write(master, &b, 1);

the characters of the command are made available for the following read, at line 80.

80: if (read(master, &c, 1) != -1)
...

All the data received from the master file descriptor is sent to STDOUT in the terminal emulator; this data includes the output of the shell, if any was produced, as well as these characters which were written at line 90. To avoid duplicated echo of the commands input by the user I disable STDIN echo at the very beginning of main(), and I re-enable it at the end. Finally, it looks like if the shell is terminated by command 'exit', the read at line 80 will return -1, so in this case I set run to false and the terminal emulator terminates also.

Why the shell is printing the prompt while in the previous example (first post) this was not the case? Also, I was wondering whether I handled the duplicated echo issue in the correct way. Is it ok that the terminal emulator, when it reads back the data from the master file descriptor, also retrieves the input command which was responsible for that output? This is what apparently is happening at line 90, when the terminal emulator reads back from the master file descriptor also the characters which were written at line 80.

Thank you for your help, have a nice day :-)

Dario

Code:

// C++ includes
#include <cstring>

// UNIX includes
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <pty.h>
#include <termios.h>

int main(int argc, char *argv[])
{
        // Disable echo on STDIN; data sent to
        // the shell will be read back together
        // with shell output. Also, data is read
        // immediately, without waiting for a
        // delimiter.
        termios oldt, newt;
        tcgetattr(STDIN_FILENO, &oldt);
        newt = oldt;
        newt.c_lflag &= ~(ECHO | ECHONL | ICANON);
        tcsetattr(STDIN_FILENO, TCSAFLUSH, &newt);
       
        // File descriptor master is one of the
        // endpoints of the communication channel
        // between the TE (master) and the shell
        // (slave).
        int master;
       
        // The shell will be run as a separate process.
        int pid = forkpty(&master, NULL, NULL, NULL);
       
        // Check that a child process was created.
        if (pid == -1)
        {
                write(STDOUT_FILENO, "Can not forkpty.\n", strlen("Can not forkpty.\n"));
                return -1;
        }
       
        // From now on the same source code will be
        // shared by two distinct processes; in the
        // parent process forkpty() returns the PID of
        // the child, while in the child process
        // it returns 0.
       
        if (pid != 0)
        {
                // The code in this if will be executed by the parent.
                // This is the TE logic.
               
                // Whether the TE should keep running.
                bool run = true;
               
                // TE sends/receives data to/from the shell one char at a time.
                char b, c;
               
                while (run)
                {
                        // Data structures needed for select() and
                        // non-blocking I/O.
                        fd_set rfd;
                        fd_set wfd;
                        fd_set efd;
                        timeval tv;

                        FD_ZERO(&rfd);
                        FD_ZERO(&wfd);
                        FD_ZERO(&efd);
                        FD_SET(master, &rfd);
                        FD_SET(STDIN_FILENO, &rfd);
                        tv.tv_sec = 0;
                        tv.tv_usec = 100000;
                        select(master + 1, &rfd, &wfd, &efd, &tv);
                       
                        // Check for data to receive; the received
                        // data includes also the data previously sent
                        // on the same master descriptor (line 90).
                        if (FD_ISSET(master, &rfd))
                        {
                                if (read(master, &c, 1) != -1)
                                        write(STDOUT_FILENO, &c, 1);
                                else
                                        run = false;
                        }
                       
                        // Check for data to send.
                        if (FD_ISSET(STDIN_FILENO, &rfd))
                        {
                                read(STDIN_FILENO, &b, 1);
                                write(master, &b, 1);
                        }
                }
        }
        else
        {
                // The code in this else will be executed by the child.
                // This is the shell logic.
               
                // The executed shell is bash.
                execlp("bash", "/bin/bash", NULL);
        }
       
        // Restore initial properties of STDIN.
        oldt.c_lflag |= ECHO | ECHONL | ICANON;
        tcsetattr(STDIN_FILENO, TCSAFLUSH, &oldt);
       
        // Everything went fine.
        return 0;
}


ta0kira 09-05-2011 10:58 AM

Quote:

Originally Posted by damix (Post 4461913)
Why the shell is printing the prompt while in the previous example (first post) this was not the case? Also, I was wondering whether I handled the duplicated echo issue in the correct way. Is it ok that the terminal emulator, when it reads back the data from the master file descriptor, also retrieves the input command which was responsible for that output? This is what apparently is happening at line 90, when the terminal emulator reads back from the master file descriptor also the characters which were written at line 80.

In your first example you left a lot of pipes open that should have been closed. Specifically, after fork you need to close the end of the pipe not being used and after dup2 you need to close the old descriptor (but only if it wasn't already equal to what you dup2ed it to!) You should also use SIGTERM instead of SIGKILL since the latter eliminates the process rather than signaling it. You should also check read for a return of 0 since that means EOF. I'm not getting the duplication you're reporting; it works fine for me.
Kevin Barry

cristinaparas 12-01-2012 06:38 PM

Permission
 
Damix: may I use parts of your code for a project crediting you (assuming I have attribution info)?


All times are GMT -5. The time now is 02:09 AM.