Add Book to My BookshelfPurchase This Book Online

Appendix D - Pseudo-Terminals

UNIX Systems Programming for SVR4
David A. Curry
 Copyright © 1996 O'Reilly & Associates, Inc.

SVR4 Pseudo-Terminals
In SVR4, the race condition has been solved by creating a special “clone device” to use when allocating a master pseudo-terminal. The clone device, when opened, returns a file descriptor referring to an unused pseudo-terminal, and locks out the corresponding slave device so that it cannot be opened by another process. The process that has the master side open can then unlock the slave and open it itself.
Example D-1 shows an implementation of the script command. This program executes a copy of the user's shell on a pseudo-terminal, and copies all the user's input and output to a file, creating a record of the entire session.
Example D-1:  script
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <stropts.h>
#include <termios.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
/* HP-UX forgets to define this */
char *ptsname(int);
#define MAXARGS 32                          /* max. cmd. args     */
char        *shell = "/bin/sh";             /* default shell      */
char        *filename = "scriptfile";       /* default file       */
char        *mastername = "/dev/ptmx";      /* pty clone device   */
int     master;                             /* master side of pty */
FILE        *script;                        /* script file        */
struct termios  newtty, origtty;            /* tty modes          */
void    finish(int);
int ptyopen(char *, struct termios *);
int
main(int argc, char **argv)
{
    char *p;
    int n, nfd;
    time_t clock;
    fd_set readmask;
    char buf[BUFSIZ];
    struct sigaction sact;
    /*
     * If an argument is given, it's a new script file.
     */
    if (argc > 1)
        filename = *++argv;
    /*
     * 1. Use the user's shell, if known.
     */
    if ((p = getenv("SHELL")) != NULL)
        shell = p;
    /*
     * 2. Open the script file.
     */
    if ((script = fopen(filename, "w")) == NULL) {
        perror(filename);
        exit(1);
    }
    /*
     * 3. Get the tty modes.  We'll use these both to
     *    set modes on the pseudo-tty, and to restore
     *    modes on the user's tty.
     */
    if (tcgetattr(0, &origtty) < 0) {
        perror("tcgetattr: stdin");
        exit(1);
    }
    /*
     * 4. Grab a pseudo-tty and start a shell on it.
     */
    if ((master = ptyopen(shell, &origtty)) < 0)
        exit(1);
    /*
     * Print a little start message.
     */
    time(&clock);
    fprintf(script, "Script started on %s", ctime(&clock));
    printf("Script started, file is %s\n", filename);
    /*
     * 5. We need to catch signals, now that we're going
     *    to change tty modes.
     */
    memset((char *) &sact, 0, sizeof(struct sigaction));
    sact.sa_handler = finish;
    sigaction(SIGINT, &sact, (struct sigaction *) 0);
    sigaction(SIGQUIT, &sact, (struct sigaction *) 0);
    /*
     * 6. Change the user's tty modes such that pretty
     *    much everything gets passed through to the
     *    pseudo-tty.  Set "raw" mode so that we can pass
     *    characters as they're typed, etc.
     */
    newtty = origtty;
    newtty.c_cc[VMIN] = 1;
    newtty.c_cc[VTIME] = 0;
    newtty.c_oflag &= ~OPOST;
    newtty.c_lflag &= ~(ICANON|ISIG|ECHO);
    newtty.c_iflag &= ~(INLCR|IGNCR|ICRNL|IUCLC|IXON);
    /*
     * 7. Set the new tty modes.
     */
    if (tcsetattr(0, TCSANOW, &newtty) < 0) {
        perror("tcsetattr: stdin");
        exit(1);
    }
    /*
     * 8. Now just sit in a loop reading from the keyboard and
     *    writing to the pseudo-tty, and reading from the
     *    pseudo-tty and writing to the screen and the script file.
     */
    for (;;) {
        FD_ZERO(&readmask);
        FD_SET(master, &readmask);
        FD_SET(0, &readmask);
        nfd = master + 1;
        /*
         * 8a. Wait for something to read.
         */
        n = select(nfd, (int *) &readmask, (int *) 0, (int *) 0,
                   (struct timeval *) 0);
        if (n < 0) {
            perror("select");
            exit(1);
        }
        /*
         * 8b. The user typed something... read it and pass
         *      it on to the pseudo-tty.
         */
        if (FD_ISSET(0, &readmask)) {
            if ((n = read(0, buf, sizeof(buf))) < 0) {
                perror("read: stdin");
                exit(1);
            }
            /*
             * The user typed end-of-file; we're
             * done.
             */
            if (n == 0)
                finish(0);
            if (write(master, buf, n) != n) {
                perror("write: pty");
                exit(1);
            }
        }
        /*
         * 8c. There's output on the pseudo-tty... read it and
         *     pass it on to the screen and the script file.
         */
        if (FD_ISSET(master, &readmask)) {
            /*
             * The process died.
             */
            if ((n = read(master, buf, sizeof(buf))) <= 0)
                finish(0);
            fwrite(buf, sizeof(char), n, script);
            write(1, buf, n);
        }
    }
}
/*
* ptyopen - start command on a pseudo-tty and return a file descriptor
*       with which to speak to it.
*/
int
ptyopen(char *command, struct termios *ttymodes)
{
    char *p;
    pid_t pid;
    char *slavename;
    char *args[MAXARGS];
    int nargs, master, slave;
    /*
     * 9. Break the command into arguments.
     */
    nargs = 0;
    p = strtok(command, " \t\n");
    do {
        if (nargs == MAXARGS) {
            fprintf(stderr, "too many arguments.\n");
            return(-1);
        }
       
        args[nargs++] = p;
        p = strtok(NULL, " \t\n");
    } while (p != NULL);
    args[nargs] = NULL;
    /*
     * 10. Get a master pseudo-tty.
     */
    if ((master = open(mastername, O_RDWR)) < 0) {
        perror(mastername);
        return(-1);
    }
    /*
     * 11. Set the permissions on the slave.
     */
    if (grantpt(master) < 0) {
        perror("granpt");
        close(master);
        return(-1);
    }
    /*
     * 12. Unlock the slave.
     */
    if (unlockpt(master) < 0) {
        perror("unlockpt");
        close(master);
        return(-1);
    }
    /*
     * 13. Start a child process.
     */
    if ((pid = fork()) < 0) {
        perror("fork");
        close(master);
        return(-1);
    }
    /*
     * 14. The child process will open the slave, which will become
     *     its controlling terminal.
     */
    if (pid == 0) {
        /*
         * 14a. Get rid of our current controlling terminal.
         */
        setsid();
        /*
         * 14b. Get the name of the slave pseudo-tty.
         */
        if ((slavename = ptsname(master)) == NULL) {
            perror("ptsname");
            close(master);
            exit(1);
        }
        /*
         * 14c. Open the slave pseudo-tty.
         */
        if ((slave = open(slavename, O_RDWR)) < 0) {
            perror(slavename);
            close(master);
            exit(1);
        }
        /*
         * 14d. Push the hardware emulation module.
         */
        if (ioctl(slave, I_PUSH, "ptem") < 0) {
            perror("ioctl: ptem");
            close(master);
            close(slave);
            exit(1);
        }
        /*
         * 14e. Push the line discipline module.
         */
        if (ioctl(slave, I_PUSH, "ldterm") < 0) {
            perror("ioctl: ldterm");
            close(master);
            close(slave);
            exit(1);
        }
        /*
         * 14f. Copy the user's terminal modes to the slave
         *      pseudo-tty.
         */
        if (tcsetattr(slave, TCSANOW, ttymodes) < 0) {
            perror("tcsetattr: pty");
            close(master);
            close(slave);
            exit(1);
        }
        /*
         * 14g. Close the script file and the master; these
         *      are not needed in the slave.
         */
        fclose(script);
        close(master);
        /*
         * 14h. Set the slave to be our standard input, output,
         *      and error output.  Then get rid of the original
         *      file descriptor.
         */
        dup2(slave, 0);
        dup2(slave, 1);
        dup2(slave, 2);
        close(slave);
        /*
         * 14i. Execute the command.
         */
        execv(args[0], args);
        perror(args[0]);
        exit(1);
    }
    /*
     * 15. Return the file descriptor for communicating with
     *     the process to our caller.
     */
    return(master);
}
       
/*
* finish - called when we're done.
*/
void
finish(int sig)
{
    time_t clock;
    /*
     * 16. Restore our original tty modes.
     */
    if (tcsetattr(0, TCSANOW, &origtty) < 0)
        perror("tcsetattr: stdin");
    /*
     * Print a finishing message.
     */
    time(&clock);
    fprintf(script, "\nScript finished at %s", ctime(&clock));
    printf("\nScript done, file is %s\n", filename);
    /*
     * 17. All done.
     */
    fclose(script);
    close(master);
    exit(0);
}
The steps executed in this program are as follows.
 1.Use the getenv function (Chapter 16, Miscellaneous Routines) to obtain the name of the user's shell. If this cannot be determined, use /bin/sh as the default.
 2.Create the script file, where all input and output will be recorded.
 3.Get the modes of the user's terminal (Chapter 12, Terminals). These are needed both to copy them to the pseudo-terminal, and to change them on the user's terminal.
 4.Call the ptyopen function to allocate a pseudo-terminal and start the shell on it. This function is described beginning with Step 9, below.
 5.Catch the interrupt and quit signals (the ones that can be generated from the keyboard). We need to do this before we change the user's terminal modes; once they are changed, catching these signals will allow us to restore them if an interrupt is received.
 6.Change the user's terminal modes (Chapter 12). Because the keyboard and screen will now be tied to the pseudo-terminal through our program, most of the terminal input/output processing on the user's real terminal needs to be disabled. In particular, ECHO needs to be turned off (since the operating system will echo all characters “typed” on the pseudo-terminal, the controlling process will see them as “output” on the pseudo-terminal). The terminal is also placed in “raw” mode so that as each character is typed it will be read and delivered to the pseudo-terminal.
 7.Actually change the user's terminal modes.
 8.The controlling program now enters the following loop:
 a)The select function (Chapter 6, Special-Purpose File Operations) is used to monitor both the standard input (the keyboard) and the “screen” of the pseudo-terminal. The function will block until something is available to be read.
 b)If the standard input (file descriptor 0) appears in the bitmask returned by select, this means the user has typed something on the keyboard. The program must read this, and then write it to the pseudo-terminal. The process attached to the pseudo-terminal will see this as “keyboard” input. Note that the user's input is not written to the script file here; if the pseudo-terminal has ECHO turned on, the operating system will echo the characters and they will be seen as output.
 c)If the pseudo-terminal file descriptor appears in the bitmask returned by select, this means the program attached to the pseudo-terminal has written some output to its “screen.” The controlling program must read this data and print it to the user's screen, and also copy it to the script file.
The program continues in this loop until a read from either the user's terminal or the pseudo-terminal returns 0, indicating either that the user has typed an end-of-file character, or the program on the pseudo-terminal has exited.
 9.Call the ptyopen function to execute the pseudo-terminal allocation code. The function begins by breaking the command it is to execute into individual arguments.
 10.Open the clone device, /dev/ptmx, to begin pseudo-terminal allocation. If the open succeeds, it will return a file descriptor that may be used to read and write to the master side of an unused pseudo-terminal.
 11.Call the grantpt function to change the modes and ownership of the slave pseudo-terminal device to those of the user calling the functon:
    #include <stdlib.h>
    int grantpt(int fd);
The argument should be the file descriptor attached to the master pseudo-terminal. The granpt function works by executing a small set-user-id “root” program to do its work.
 12.Use the unlockpt function to clear the lock on the slave pseudo-terminal device, so that it can be opened:
    #include <stdlib.h>
    int unlockpt(int fd);
Again, the argument should be the file descriptor attached to the master pseudo-terminal.
 13.Now a child process is started, to execute the command given as an argument to ptyopen (Chapter 11, Processes).
The child process is responsible for opening the slave side of the pseudo-terminal and executing the command:
 a)The setsid function (Chapter 11) is called to begin a new session. This has the side effect of clearing the process' controlling terminal.
 b)The ptsname function returns the device name of the slave side of the pseudo-terminal:
        #include <stdlib.h>
        char *ptsname(int fd);
The fd parameter should be the file descriptor attached to the master side of the pseudo-terminal.
 c)The slave side of the pseudo-terminal is opened. As a side effect of this, because the process has no controlling terminal (it was cleared by setsid), the slave device will become the process' controlling terminal. This means that any signals generated from the slave side's “keyboard” will be sent to the slave process, since it is the session leader.
 d)The “ptem” module is pushed onto the stream from the pseudo-terminal. This is a module built into the kernel that allows the pseudo-terminal to emulate a real terminal. It intercepts all the terminal mode change requests and adjusts the pseudo-terminal driver to behave accordingly.
 e)The “ldterm” module is pushed onto the stream from the pseudo-terminal. This is a module built into the kernel that allows the pseudo-terminal to emulate the line discipline functions (Chapter 12) associated with real terminal devices.
 f)The user's terminal modes are copied to the pseudo-terminal.
 g)The script file and master pseudo-terminal file descriptors, opened in the parent process, are closed. The child process has no use for these.
 h)The dup2 function (Chapter 3, Low-Level I/O Routines) is used to attach the child process' standard input, output, and error output to the slave pseudo-terminal. The original file descriptor is then closed, as it is no longer needed.
 i)The command is executed. When this succeeds, the command will be running on the slave pseudo-terminal (which it will see as a real terminal), and the command's input and output will be attached to the controlling process through the master side of the pseudo-terminal.
 14.The file descriptor attached to the master side of the pseudo-terminal is returned to the controlling process, which can now use it to communicate with the command.
Once the command on the pseudo-terminal has exited or the user has typed end-of-file, the program restores the user's original terminal modes.
It then closes the script file, and closes the master pseudo-terminal. If the process on the pseudo-terminal has not yet exited, this close will generate an end-of-file on its input, causing it to exit now.
The clone-device method of allocating pseudo-terminals is generally easier to use than the old Berkeley method. It is not the only solution though; other vendors have developed other methods for opening pseudo-terminals. However, most of them are similar to one of the two methods described here, and differ only in some minor details.

Previous SectionNext Section
Books24x7.com, Inc © 2000 –  Feedback