A french version of this article has been published in GNU Linux Magazine France, issue#100.
Although less and less popular, the interactive applications in command
line mode interacting with an operator through a terminal on a serial
port, are still widely used. Especially in the embedded world where the
graphical resources are superfluous or too expensive. Among those
applications, we can quote as examples:
Once a program is loaded into memory to be executed, it becomes a process attached to the current terminal. By default, the standard input (stdin) comes from the keyboard and the standard outputs (stdout and stderr) are redirected to the screen (cf. figure 1).
Linux
provides the ability to redirect the standard input and outputs in
order to get data from another source than the keyboard and display
data to another destination than the screen. This feature is very
powerful: a process reads its standard input and writes to its standard
outputs without knowing any details about the devices. In other words,
a program can run without any modifications to read from the keyboard
or a file or the output of another process (pipe mechanism). It
is the same for the outputs.
Let's consider the following program called mylogin
which gets a login name and a password:
#include <stdio.h> int main(void) { char login_name[150]; char password[150]; // By default stdin, stdout and stderr are open fprintf(stdout, "Login : "); if (NULL == fgets(login_name, sizeof(login_name), stdin)) { fprintf(stderr, "No login name\n"); return 1; } fprintf(stdout, "Password : "); if (NULL == fgets(password, sizeof(password), stdin)) { fprintf(stderr, "No password\n"); return 1; } fprintf(stdout, "Result :\n%s%s\n", login_name, password); return 0; } |
Under a shell like bash, multiple solutions are available to make redirections. If we launch the program as it is, the input is the keyboard and the outputs are the screen.
$ ./mylogin Login : bar Password : foo Result : bar foo $ |
The preceding program can be launched as follow to redirect the output to the file output.txt.
$ ./mylogin > output.txt bar foo $ cat output.txt Login : Password : Result : bar foo $ |
We can see that without any modifications in the program mylogin, it has been possible to launch it to have the standard output redirected to the screen and then to the file output.txt.
A program as simple as mylogin can be automated. That is to say that the human operator can be replaced by a program like a shell script. Let's consider the input.txt file into which are stored the answers expected by mylogin.
$ cat input.txt bar foo $ |
We can launch mylogin with input.txt as standard input:
$ ./mylogin < input.txt Login: Password : Result : bar foo $ |
So, we have replaced the human operator by a file containing the expected entries. Unfortunately, it is not possible to apply this method to all interactive programs. Some of them are very elaborated. A program reading a password typically flushes its standard input right after having displayed the password prompt to get rid of any characters entered between the login name and the password prompt (the character echoing is also deactivated during the password entry). We can illustrate this by making mylogin flush its standard input just before the password entry (call to fseek()).
#include <stdio.h> int main(void) { char login_name[150]; char password[150]; // By default stdin, stdout and stderr are open fprintf(stdout, "Login : "); if (NULL == fgets(login_name, sizeof(login_name), stdin)) { fprintf(stderr, "No login name\n"); return 1; } // Flush standard input fseek(stdin, 0, SEEK_END); fprintf(stdout, "Password : "); if (NULL == fgets(password, sizeof(password), stdin)) { fprintf(stderr, "No password\n"); return 1; } fprintf(stdout, "Result :\n%s%s\n", login_name, password); return 0; } |
When interacting with the operator, the program behaves the same (the keyboard is the standard input).
$ ./mylogin login : bar Password : foo Result : bar foo |
On the other side, when the standard input is a file, the error message "No password" is displayed. Actually, the second call to fread() gets an end of file which means that there are no more data on input.
$ ./mylogin < input.txt
No password
Login : Password :
$
|
When the operator interacts, he waits for
the display of the password prompt before entering the password. When
the entry is the file input.txt,
the
entire file is
entered
at
the
beginning
of
the
program.
So,
the
first
line is read with the first call to fread()
and the
second line is flushed by the call to fseek().
That's
why
the
second
fread()
encounters an end of file. This is a typical case where the program is
desynchronized with its standard input.
From
this example, we encountered one of the numerous problem we can have
while attempting to automate an interactive program. These kind of
programs also suppose that their standard input and output are
terminals. So, they can trigger some terminal specific operations like
"echo off", "canonical mode", "line mode"... If the input and output
are not terminals but files for examples, these operations will fail and
trigger errors in the program.
The pseudo-terminal concept is a solution to those problems.
A pseudo-terminal is a pair of character mode devices also called pty. One is master and the other is slave and they are connected with a bidirectional channel. Any data written on the slave side is forwarded to the output of the master side. Conversely, any data written on the master side is forwarded to the output of the slave side as depicted in figure 2.
The
slave side behaves exactly as a standard terminal as any process can
open it to make it its standard input and outputs. So, all the
operations like disabling the echo, setting the line mode or canonical
mode are available.
The master side is not a terminal. It is just a device which permits to
send/receive data to/from the slave side.
In
the Unix world, there are multiple implementations of the
pseudo-terminals. There are the BSD and the System V versions. The
Linux world recommends the system V implementation also called "Unix 98 pty". This
is the one we are going to study below.
The API is quite simple:
#define _XOPEN_SOURCE 600 #include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <errno.h> int main(void) { int fdm; int rc; // Display /dev/pts system("ls -l /dev/pts"); fdm = posix_openpt(O_RDWR); if (fdm < 0) { fprintf(stderr, "Error %d on posix_openpt()\n", errno); return 1; } rc = grantpt(fdm); if (rc != 0) { fprintf(stderr, "Error %d on grantpt()\n", errno); return 1; } rc = unlockpt(fdm); if (rc != 0) { fprintf(stderr, "Error %d on unlockpt()\n", errno); return 1; } // Display the changes in /dev/pts system("ls -l /dev/pts"); printf("The slave side is named : %s\n", ptsname(fdm)); return 0; } // main |
The program lists the content of the directory /dev/pts at the beginning and at the end to show the creation of the slave side. In the following example, this is the slave number 4 which is created:
$ ./mypty total 0 crw--w---- 1 koucha tty 136, 0 2007-09-25 13:56 0 crw--w---- 1 koucha tty 136, 1 2007-09-25 13:32 1 crw--w---- 1 koucha tty 136, 2 2007-09-25 12:58 2 crw--w---- 1 koucha tty 136, 3 2007-09-25 07:32 3 total 0 crw--w---- 1 koucha tty 136, 0 2007-09-25 13:56 0 crw--w---- 1 koucha tty 136, 1 2007-09-25 13:32 1 crw--w---- 1 koucha tty 136, 2 2007-09-25 12:58 2 crw--w---- 1 koucha tty 136, 3 2007-09-25 07:32 3 crw--w---- 1 koucha tty 136, 4 2007-09-25 13:56 4 The slave side is named : /dev/pts/4 $ |
A pseudo-terminal is mainly used to make a process believe that it interacts with a human operator although it actually interacts with one or more processes.
To point out the pseudo-terminal functions, we can modify mypty into mypty2.
#define _XOPEN_SOURCE 600 #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <unistd.h> #include <stdio.h> #define __USE_BSD #include <termios.h> int main(void) { int fdm, fds, rc; char input[150]; fdm = posix_openpt(O_RDWR); if (fdm < 0) { fprintf(stderr, "Error %d on posix_openpt()\n", errno); return 1; } rc = grantpt(fdm); if (rc != 0) { fprintf(stderr, "Error %d on grantpt()\n", errno); return 1; } rc = unlockpt(fdm); if (rc != 0) { fprintf(stderr, "Error %d on unlockpt()\n", errno); return 1; } // Open the slave PTY fds = open(ptsname(fdm), O_RDWR); // Creation of a child process if (fork()) { // Father // Close the slave side of the PTY close(fds); while (1) { // Operator's entry (standard input = terminal) write(1, "Input : ", sizeof("Input : ")); rc = read(0, input, sizeof(input)); if (rc > 0) { // Send the input to the child process through the PTY write(fdm, input, rc); // Get the child's answer through the PTY rc = read(fdm, input, sizeof(input) - 1); if (rc > 0) { // Make the answer NUL terminated to display it as a string input[rc] = '\0'; fprintf(stderr, "%s", input); } else { break; } } else { break; } } // End while } else { struct termios slave_orig_term_settings; // Saved terminal settings struct termios new_term_settings; // Current terminal settings // Child // Close the master side of the PTY close(fdm); // Save the default parameters of the slave side of the PTY rc = tcgetattr(fds, &slave_orig_term_settings); // Set raw mode on the slave side of the PTY new_term_settings = slave_orig_term_settings; cfmakeraw (&new_term_settings); tcsetattr (fds, TCSANOW, &new_term_settings); // The slave side of the PTY becomes the standard input and outputs of the child process close(0); // Close standard input (current terminal) close(1); // Close standard output (current terminal) close(2); // Close standard error (current terminal) dup(fds); // PTY becomes standard input (0) dup(fds); // PTY becomes standard output (1) dup(fds); // PTY becomes standard error (2) while (1) { rc = read(fds, input, sizeof(input) - 1); if (rc > 0) { // Replace the terminating \n by a NUL to display it as a string input[rc - 1] = '\0'; printf("Child received : '%s'\n", input); } else { break; } } // End while } return 0; } // main |
The program consists of two processes. The father reads a string from the keyboard and writes it on the master side of the pty. The child replaced its standard input and outputs by the slave side of the pty. It reads the slave side and displays what it received on the slave side prefixed by "Child received : ". Here is an example of execution:
$ ./mypty2 Input : azerty Child received : 'azerty' Input : qwerty Child received : 'qwerty' Input : pwd Child received : 'pwd' |
Figure 3 depicts the behaviour of the program when the operator enters "qwerty".
On the slave side we can note the calls to cfmakeraw() and tcsetattr() to
reconfigure the slave side of the pty.
This
sets
the
raw
mode
to
disable
the
echoing
among
other
things.
We can make mypty2
more generic in order to be able to execute any program
behind the pty
(slave side). In mypty3,
the father process writes all the data from its standard input to the
master side of the pty
and writes all the data from the master side of the pty to its standard
output. The child process behaves the same as in mypty2
but executes an interactive program along with its parameters passed as arguments to the program.
We can note the calls to setsid()
and ioctl(TIOCSCTTY) to make
the pty
be the control terminal of the executed program. We can also note the
closing of the fds file
descriptor which becomes useless after the calls to dup().
#define _XOPEN_SOURCE 600 #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <unistd.h> #include <stdio.h> #define __USE_BSD #include <termios.h> #include <sys/select.h> #include <sys/ioctl.h> #include <string.h> int main(int ac, char *av[]) { int fdm, fds; int rc; char input[150]; // Check arguments if (ac <= 1) { fprintf(stderr, "Usage: %s program_name [parameters]\n", av[0]); exit(1); } fdm = posix_openpt(O_RDWR); if (fdm < 0) { fprintf(stderr, "Error %d on posix_openpt()\n", errno); return 1; } rc = grantpt(fdm); if (rc != 0) { fprintf(stderr, "Error %d on grantpt()\n", errno); return 1; } rc = unlockpt(fdm); if (rc != 0) { fprintf(stderr, "Error %d on unlockpt()\n", errno); return 1; } // Open the slave side ot the PTY fds = open(ptsname(fdm), O_RDWR); // Create the child process if (fork()) { fd_set fd_in; // FATHER // Close the slave side of the PTY close(fds); while (1) { // Wait for data from standard input and master side of PTY FD_ZERO(&fd_in); FD_SET(0, &fd_in); FD_SET(fdm, &fd_in); rc = select(fdm + 1, &fd_in, NULL, NULL, NULL); switch(rc) { case -1 : fprintf(stderr, "Error %d on select()\n", errno); exit(1); default : { // If data on standard input if (FD_ISSET(0, &fd_in)) { rc = read(0, input, sizeof(input)); if (rc > 0) { // Send data on the master side of PTY write(fdm, input, rc); } else { if (rc < 0) { fprintf(stderr, "Error %d on read standard input\n", errno); exit(1); } } } // If data on master side of PTY if (FD_ISSET(fdm, &fd_in)) { rc = read(fdm, input, sizeof(input)); if (rc > 0) { // Send data on standard output write(1, input, rc); } else { if (rc < 0) { fprintf(stderr, "Error %d on read master PTY\n", errno); exit(1); } } } } } // End switch } // End while } else { struct termios slave_orig_term_settings; // Saved terminal settings struct termios new_term_settings; // Current terminal settings // CHILD // Close the master side of the PTY close(fdm); // Save the defaults parameters of the slave side of the PTY rc = tcgetattr(fds, &slave_orig_term_settings); // Set RAW mode on slave side of PTY new_term_settings = slave_orig_term_settings; cfmakeraw (&new_term_settings); tcsetattr (fds, TCSANOW, &new_term_settings); // The slave side of the PTY becomes the standard input and outputs of the child process close(0); // Close standard input (current terminal) close(1); // Close standard output (current terminal) close(2); // Close standard error (current terminal) dup(fds); // PTY becomes standard input (0) dup(fds); // PTY becomes standard output (1) dup(fds); // PTY becomes standard error (2) // Now the original file descriptor is useless close(fds); // Make the current process a new session leader setsid(); // As the child is a session leader, set the controlling terminal to be the slave side of the PTY // (Mandatory for programs like the shell to make them manage correctly their outputs) ioctl(0, TIOCSCTTY, 1); // Execution of the program { char **child_av; int i; // Build the command line child_av = (char **)malloc(ac * sizeof(char *)); for (i = 1; i < ac; i ++) { child_av[i - 1] = strdup(av[i]); } child_av[i - 1] = NULL; rc = execvp(child_av[0], child_av); } // if Error... return 1; } return 0; } // main |
Below, we launched mypty3 with the calculator bc as program.
$ ./mypty3 Usage: ./mypty3 program_name $ $ ./mypty3 bc bc 1.06 Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc. This is free software with ABSOLUTELY NO WARRANTY. For details type `warranty'. 3+6 9 quit Erreur 5 on read master PTY $ |
It is possible to launch a shell or any other interactive program. This behaviour applies to numerous famous programs like xterm, telnet, ftp, rlogin, rsh... For example, figure 4 depicts the architecture of the telnet program.
The telnetd process is the father process. Its standard input and outputs are not a terminal but a network connection to a remote telnet client. The child process is a bash shell. All the data coming from the client through the network connection is forwarded by telnetd to the master side of the pty. All the data coming from the bash shell through the pty is forwarded by telnetd to the remote client.
If we read carefully the online manual of grantpt(), it is said that "the behavior of grantpt() is unspecified if a signal handler is installed to catch SIGCHLD signals". The reason for that can be found by reading its source code in the GLIBC. grantpt() can end with a call to fork() to execute chown program while the father (i.e. the caller of grantpt()) waits for its termination through waitpid(). Hence one cannot capture SIGCHLD signal otherwise the waitpid() into grantpt() will fail. Here is an snippet of the end of the source code of grantpt() :
/* Change the ownership and access permission of the slave pseudo terminal associated with the master pseudo terminal specified by FD. */ int grantpt (int fd) { [...] /* We have to use the helper program. */ helper:; pid_t pid = __fork (); if (pid == -1) goto cleanup; else if (pid == 0) { /* Disable core dumps. */ struct rlimit rl = { 0, 0 }; __setrlimit (RLIMIT_CORE, &rl); /* We pass the master pseudo terminal as file descriptor PTY_FILENO. */ if (fd != PTY_FILENO) if (__dup2 (fd, PTY_FILENO) < 0) _exit (FAIL_EBADF); #ifdef CLOSE_ALL_FDS CLOSE_ALL_FDS (); #endif execle (_PATH_PT_CHOWN, basename (_PATH_PT_CHOWN), NULL, NULL); _exit (FAIL_EXEC); } else { int w; if (__waitpid (pid, &w, 0) == -1) goto cleanup; [...] |
So, one should disable any SIGCHLD handler before calling grantpt() and reenable it right after the call.
mypty3 can be more intelligent by making it interpret a command language to synchronize with the interactive program. In other words, we could replace the human operator by a script of commands. This is what a program like pdip does.
pdip
stands for "Programmed
Dialogue
with Interactive
Programs".
The acronym PDIP comes from the first lines of the manual of expect.
Like expect,
it interprets a scripting language to dialog with an interactive
program as a human operator would do. But it has not all the
bells
and whistles of expect
which is
able to interact with multiple programs at the same
time, accept a high level scripting language providing
branching
and high level control structures or giving back the control to the
operator during a session. pdip accepts a
very simple language on the input to provide basic functions such as:
pdip accepts the following commands:
Using pdip is straightforward as we can see with the control of a telnet client which connects to a host called ’remote’ on the TCP port 34770 with the login name ’foo’ and password ’bar’. Since the remote port is specified with an option (-p), it is mandatory to put a double hyphen (--) before the command to launch. Commands are injected on the standard input. We wait for the ’$’ prompt and launch the ls(1) command before disconnecting from the shell via the exit command.
$ pdip -- telnet -p 34770 remote recv "login" send "foo\n" # Login name is -F¢foo¢-A recv "Password" send "bar\n" # Password is -F¢bar¢-A recv "\$ " # Inhibition of the metacharacter -F¢$¢ with ¢\¢-A send "ls\n" # Launch the -F¢ls¢ command-A recv "\$ " send "exit\n" # Exit from the shell exit # Exit from PDIP $ We can note that it is mandatory to quote the "$" sign on the recv command as it is a metacharacter meaning end of line. Here is an example of execution:
|