hn-classics/_stories/2010/14527671.md

14 KiB

created_at title url author points story_text comment_text num_comments story_id story_title story_url parent_id created_at_i _tags objectID year
2017-06-10T15:26:16.000Z Using Pseudo-Terminals to Control Interactive Programs (2010) http://rachid.koucha.free.fr/tech_corner/pty_pdip.html robertelder 49 10 1497108376
story
author_robertelder
story_14527671
14527671 2010

Source

Using pseudo-terminals to control interactive programs, pty, pdip

Author: R. Koucha
Last update: 16-Apr-2014

Back to home page
Back to previous page

Using pseudo-terminals (pty) to control interactive programs

| ----- | |
|

| ----- | |

|
|
|
|
|
|
| s |

Foreword
Introduction
Redirection of the standard input and outputs of a process
Problems while automating interactive programs
Introduction to pseudo-terminals

API of the pseudo-terminals

Usage of the pseudo-terminals

Inter-process communication through a pseudo-terminal
Limitation of grantpt()
Taking control over an interactive process

Presentation of pdip

Using pdip

Resources
About the author

Foreword

A french version of this article has been published in glmf_logo special issue 34.

Introduction

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:

  • bash: the default Linux shell
  • bc: the calculator
  • ftp: file transfer utility
  • telnet: remote terminal It is possible to use those tools in shell scripts in order to automate some tasks like tests or system maintenance and administration. For example, we could use telnet in a script to connect to a remote machine to trigger some operations. Unfortunately, this is not that simple because an interactive program needs a human to operate.

This article focuses on a solution based on pseudo-terminals to automate interactive programs.

Redirection of the standard input and outputs of a process

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).

Figure 1: Standard input and outputs of a Linux process

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 namen");
return 1;
}

fprintf(stdout, "Password : ");
if (NULL == fgets(password, sizeof(password), stdin))
{
fprintf(stderr, "No passwordn");
return 1;
}

fprintf(stdout, "Result :n%s%sn", 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.

Problems while automating interactive programs

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 file input.txt into which we have 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 namen");
return 1;
}

// Flush standard input
fseek(stdin, 0, SEEK_END);

fprintf(stdout, "Password : ");
if (NULL == fgets(password, sizeof(password), stdin))
{
fprintf(stderr, "No passwordn");
return 1;
}

fprintf(stdout, "Result :n%s%sn", 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
La saisie est :
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 entired 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 example, these operations will fail and trigger errors in the program.
The pseudo-terminal concept is a solution to those problems.

Introduction to pseudo-terminals

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.

Figure 2: Overview of a pseudo-terminal

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 implementation 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.

API of the pseudo-terminals

The API is quite simple:

  • posix_openpt()
    This call creates the master side of the pty. It opens the device /dev/ptmx to get the file descriptor belonging to the master side.
  • grantpt()
    The file descriptor got from posix_openpt() is passed to grantpt() to change the access rights on the slave side: the user identifier of the device is set to the user identifier of the calling process. The group is set to an unspecified value (e.g. "tty") and the access rights are set to crx--w----.
  • unlockpt()
    After grantpt(), the file descriptor is passed to unlockpt() to unlock the slave side.
  • ptsname()
    In order to be able to open the slave side, we need to get its file name through ptsname(). To this API, we can mention the standard API of the terminals: tcgetattr(), cfmakeraw()...

The following program called mypty uses the API to create a pseudo-terminal.

| ----- | | #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 : %sn", ptsname(fdm));

return 0;
} // main |

The programs 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
$ |

Usage of the pseudo-terminals

A pseudo-terminal is mainly used to make a process believe that it interacts with a terminal although it actually interacts with one or more processes.

Inter-process communication through a pseudo-terminal

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] = '