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

Page principale
Page précédente

Utilisation des pseudo-terminaux (pty) pour piloter les programmes interactifs









s


Bien que de moins en moins utilisées, les applications intéractives en mode ligne de commande interagissant avec un opérateur via un terminal sur port série, sont encore légions. Notamment dans le monde Linux embarqué où les ressources graphiques sont superflues ou d'un coût trop élevé. Parmi ces applications, on peut en citer quelques unes des plus connues:

Il est possible de tirer bénéfice de ces utilitaires dans des scripts shell afin d'automatiser certaines tâches comme les tests ou les opérations de maintenance et d'administration système. Par exemple, on pourrait lancer un script qui crée une session telnet sur une machine distante afin de déclencher certaines opérations. Mais cela n'est pas aussi simple car un processus interactif, nécessitant l'intervention d'un opérateur pour fonctionner, se prête à priori mal à une automatisation de son déclenchement.

Cet article se propose donc de présenter une solution à l'automatisation des programmes interactifs à travers la notion de pseudo-terminal.

Avant propos
1. Redirection des entrées et sorties standards d'un processus
2. Problèmes d'automatisation d'un programme intéractif
3. Présentation des pseudo-terminaux
3.1 API des pseudo-terminaux 
4. Application des pseudo-terminaux
4.1 Communication inter-processus via un pseudo-terminal
4.2 Limitation de grantpt()
4.3 Prise de contrôle d'un processus intéractif
5. Présentation de PDIP
6. Utilisation de PDIP
Conclusion
Notes
A propos de l'auteur

Avant propos

Cet article a été publié dans glmf_logo numéro hors série 34.

1. Redirection des entrées et sorties standards d'un processus

Un programme Linux lorsqu'il est chargé en mémoire pour être exécuté, devient un processus qui est attaché au terminal courant. Par défaut, l'entrée standard (stdin) provient du clavier tandis que les sorties normales et en erreur standards (stdout et stderr) sont redirigées sur l'écran (cf. figure 1).

Figure 1 : Entrée et sorties standard d'un processus Linux

figure_1


Linux offre la notion de redirection des entrées et sorties de sorte à permettre à un processus de lire ses données d'entrée à partir d'une autre source que le clavier du terminal courant et d'afficher ses données de sortie sur une autre destination que l'écran du terminal courant. La puissance de ce mécanisme réside dans le fait que les redirections sont complètement transparentes: le processus lit son entrée standard et affiche sur ses sorties standards sans connaître la nature des périphériques qui se cachent derrière. En d'autres termes, un programme peut être lancé sans modification pour lire tantôt le clavier, tantôt le contenu d'un fichier, tantôt la sortie d'un autre programme (via le mécanisme de pipe). Il en va de même pour ses sorties.

Considérons le programme simple suivant appelé mylogin, qui saisit un nom de login et un mot de passe :

#include <stdio.h>

int main(void)
{
char nom_de_login[150];
char mot_de_passe[150];

  // Par défaut stdin, stdout et stderr sont ouverts
  fprintf(stdout, "login : ");
  if (NULL == fgets(nom_de_login, sizeof(nom_de_login), stdin))
  {
    fprintf(stderr, "Pas de nom de login\n");
    return 1;
  }

  fprintf(stdout, "Mot de passe : ");
  if (NULL == fgets(mot_de_passe, sizeof(mot_de_passe), stdin))
  {
    fprintf(stderr, "Pas de mot de passe\n");
    return 1;
  }

  fprintf(stdout, "La saisie est :\n%s%s\n", nom_de_login, mot_de_passe);

  return 0;
}


Sous un shell tel que bash, plusieurs solutions sont à disposition pour effectuer les opérations de redirection.
Si on lance le programme simplement, son entrée et ses sorties standards sont respectivement le clavier et l'écran du terminal courant :

$ ./mylogin
login : toto
Mot de passe : foo
La saisie est :
toto
foo
$


Le programme précédent peut être lancée de la manière suivante pour rediriger ce qui est saisi au clavier dans le fichier output.txt :


$ ./mylogin > output.txt
toto
foo
$ cat output.txt
login : Mot de passe : La saisie est :
toto
foo
$


On voit que sans aucunes modifications du programme mylogin, on a pu le lancer la première fois avec l'entrée standard sur le clavier et la sortie standard sur l'écran et la seconde fois avec l'entrée standard sur le clavier et la sortie standard sur le fichier output.txt.

2. Problèmes d'automatisation d'un programme intéractif

Un programme intéractif aussi simple que mylogin peut être automatisé. Nous entendons par automatisation, le remplacement d'un opérateur humain par un programme tel qu'un script shell. Considérons par exemple, le fichier input.txt dans lequel on a mis le nom de login et le mot de passe attendus par mylogin :

$ cat input.txt
toto
foo
$


On peut lancer le programme mylogin en injectant le fichier input.txt sur son entrée standard :

$ ./mylogin < input.txt
login : Mot de passe : La saisie est :
toto
foo
$


On a donc remplacé l'opérateur humain par un fichier contenant les entrées attendues par mylogin. Mais il n'est malheureusement pas possible de généraliser cette méthode à tout programme intéractif. En effet, certains sont très élaborés. Typiquement, un programme de saisie de login et mot de passe effectue systématiquement un nettoyage de son entrée standard pour ne pas tenir compte des caractères saisis entre la demande du nom de login et la demande du mot de passe (l'écho des caractères est aussi désactivée pendant la saisie du mot de passe). Pour étayer ces dernières remarques, on peut simuler le nettoyage de l'entrée standard en modifiant mylogin.c de sorte à insérer un appel à fseek() juste avant la saisie du mot de passe :

#include <stdio.h>

int main(void)
{
char nom_de_login[150];
char mot_de_passe[150];

  // Par défaut stdin, stdout et stderr sont ouverts

  fprintf(stdout, "login : ");
  if (NULL == fgets(nom_de_login, sizeof(nom_de_login), stdin))
  {
    fprintf(stderr, "Pas de nom de login\n");
    return 1;
  }

  // Nettoyage de l'entrée standard
  fseek(stdin, 0, SEEK_END);

  fprintf(stdout, "Mot de passe : ");
  if (NULL == fgets(mot_de_passe, sizeof(mot_de_passe), stdin))
  {
    fprintf(stderr, "Pas de mot de passe\n");
    return 1;
  }

  fprintf(stdout, "La saisie est :\n%s%s\n", nom_de_login, mot_de_passe);

  return 0;
}


Le programme continue à se comporter comme souhaité quand il interagit avec un opérateur (l'entrée standard est le clavier) :

$ ./mylogin

login : toto

Mot de passe : foo

La saisie est :

toto

foo


Par contre, lorsque l'entrée standard est un fichier, le message d'erreur « Pas de mode de passe » s'affiche pour indiquer qu'aucune donnée n'a été saisie pour le mot de passe. En fait, le deuxième appel à fread() rencontre une fin de fichier qui indique qu'il n'y a plus de données en entrée :

$ ./mylogin < input.txt
Pas de mot de passe
login : Mot de passe :
$


Quand les données du programme sont fournies par l'opérateur, ce dernier attend l'affichage de la chaîne « Mot de passe » avant de saisir le mot de passe. En mode automatique, le fichier input.txt est injectée d'une traite et par conséquent sa deuxième ligne se retrouve « nettoyée » par l'appel à fseek(). C'est un cas typique de désynchronisation de l'entrée standard avec le programme.

A travers cet exemple simple, est mis en avant un des nombreux problèmes que l'on peut rencontrer lors du lancement automatique des programmes intéractifs. En effet, en plus de la désynchronisation qu'il peut y avoir entre les données en entrée et le programme, ce dernier peut aussi effectuer des opérations de reconfiguration du terminal pour se mettre en mode ligne ou canonique ou tout simplement pour désactiver l'écho des caractères. Si, l'entrée ou les sorties standards ne sont pas des terminaux mais des fichiers par exemple, alors ces opérations vont échouer et déclencher des erreurs dans le programme.

La notion de pseudo-terminal est une solution à ce problème comme nous allons le voir dans la suite.

3. Présentation des pseudo-terminaux

Un pseudo-terminal (communément appelé pty) est une paire de périphériques virtuels en mode caractère : l'un est esclave et l'autre est maître. Un canal bidirectionnel relie ces deux entités. Toute donnée écrite du côté maître se retrouve en sortie du côté esclave. Inversement, toute donnée écrite du côté esclave, se retrouve en sortie du côté maître comme indiqué en figure 2.

Figure 2 : Vue générale d'un pseudo-terminal

figure_2


La partie esclave se comporte exactement comme un terminal classique dans le sens où tout processus peut l'ouvrir pour en faire son entrée et sa sortie standard. Certains traitements tel que l'écho, le remplacement des carriage return par des line feed ou autre peuvent être réalisés sur les données entrant ou sortant sur le pty esclave.

La partie maître n'est quant à elle pas un terminal mais permet de faire des opérations de lecture de données provenant de la partie esclave et d'écriture de données à destination de la partie esclave.

Dans le monde Unix en général, il existe plusieurs implémentations des pseudo-terminaux. Il y a la version BSD et la version System V. Le monde Linux a retenu la version System V sous l'appellation « Unix 98 pty ». C'est cette dernière qui est recommandée désormais et qui fera donc l'objet de la suite de cet article.


3.1 API des pseudo-terminaux 

La mise en oeuvre des pseudo-terminaux se fait à l'aide d'une API assez simple : 

Cette fonction permet de créer la partie maître d'un pseudo-terminal. Elle ouvre le périphérique /dev/ptmx pour obtenir le descripteur de fichier associé à la partie maître du pseudo-terminal.

Après l'appel à posix_openpt(), le descripteur de fichier est passé à grantpt() pour changer les droits d'accès sur la partie esclave du pseudo-terminal: l'identifiant d'utilisateur du périphérique esclave est positionné avec l'identifiant d'utilisateur du processus appelant. Le groupe est positionné à une valeur non spécifiée (par exemple « tty ») et les droits d'accès sont positionnés à crw--w----. 

Après l'appel à grantpt(), le descripteur de fichier est passé à unlockpt() pour dévérouiller le périphérique esclave.

Après les opérations précédentes, la partie esclave du pseudo-terminal peut être ouverte à l'aide de l'appel système open() mais avant cela, il faut obtenir le nom du périphérique esclave via l'appel à ptsname().

A ces API, il faut ajouter les opérations classiques sur les terminaux telles que tcgetattr(), cfmakeraw()...

Le petit programme suivant appelé mypty met en oeuvre l'API pour créer un 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;

  // Affichage de /dev/pts
  system("ls -l /dev/pts");

  fdm = posix_openpt(O_RDWR);
  if (fdm < 0)
  {
    fprintf(stderr, "Erreur %d sur posix_openpt()\n", errno);
    return 1;
  }

  rc = grantpt(fdm);
  if (rc != 0)
  {
    fprintf(stderr, "Erreur %d sur grantpt()\n", errno);
    return 1;
  }

  rc = unlockpt(fdm);
  if (rc != 0)
  {
    fprintf(stderr, "Erreur %d sur unlockpt()\n", errno);
    return 1;
  }

  // Affichage des changements dans /dev/pts
  system("ls -l /dev/pts");

  printf("Le pseudo-terminal esclave a pour nom : %s\n", ptsname(fdm));

  return 0;
} // main


Le programme affiche le contenu du répertoire /dev/pts au début et à la fin de son exécution pour montrer que la création d'un pseudo-terminal ajoute une nouvelle entrée dans le répertoire /dev/pts. Dans l'exemple d'exécution suivant, c'est le pseudo-terminal esclave numéro 4 qui est créé :

$ ./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
Le pseudo-terminal esclave a pour nom : /dev/pts/4

$


4. Application des pseudo-terminaux

Les pseudo-terminaux sont essentiellement utilisés pour faire croire à un processus qu'il est en interface avec un terminal classique alors qu'il est en communication avec un ou plusieurs processus.

4.1 Communication inter-processus via un pseudo-terminal

Pour mettre en évidence les fonctionnalités d'un pseudo-terminal, on peut modifier le programme mypty en mypty2 comme suit :

#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, "Erreur %d sur posix_openpt()\n", errno);
    return 1;
  }

  rc = grantpt(fdm);
  if (rc != 0)
  {
    fprintf(stderr, "Erreur %d sur grantpt()\n", errno);
    return 1;
  }

  rc = unlockpt(fdm);
  if (rc != 0)
  {
    fprintf(stderr, "Erreur %d sur unlockpt()\n", errno);
    return 1;
  }

  // Ouverture du PTY esclave
  fds = open(ptsname(fdm), O_RDWR);

  // Création d'un processus fils
  if (fork())
  {
    // Code du processus pere

    // Fermeture de la partie esclave du PTY
    close(fds);

    while (1)
    {
      // Saisie operateur (entree standard = terminal)
      write(1, "Entree : ", sizeof("Entree : "));

      rc = read(0, input, sizeof(input));

      if (rc > 0)
      {
        // Envoie de la saisie aux processus fils via le PTY
        write(fdm, input, rc);

        // Lecture de la reponse du fils dans le PTY
        rc = read(fdm, input, sizeof(input) - 1);

        if (rc > 0)
        {
          // Ajout d'une fin de chaine en fin de buffer
          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

    // Code du processus fils

    // Fermeture de la partie maitre du PTY
    close(fdm);

    // Sauvegarde des parametre par defaut du PTY esclave
    rc = tcgetattr(fds, &slave_orig_term_settings);

    // Positionnement du PTY esclave en mode RAW
    new_term_settings = slave_orig_term_settings;

    cfmakeraw (&new_term_settings);
    tcsetattr (fds, TCSANOW, &new_term_settings);

    // Le cote esclave du PTY devient l'entree et les sorties standards du fils
    close(0); // Fermeture de l'entrée standard (terminal courant)
    close(1); // Fermeture de la sortie standard (terminal courant)
    close(2); // Fermeture de la sortie erreur standard (terminal courant)
    dup(fds); // Le PTY devient l'entree standard (0)
    dup(fds); // Le PTY devient la sortie standard (1)
    dup(fds); // Le PTY devient la sortie erreur standard (2)

    while (1)
    {
      rc = read(fds, input, sizeof(input) - 1);
      if (rc > 0)
      {
        // Remplacement du retour a la ligne par une fin de chaine
        input[rc - 1] = '\0';

        printf("Le fils a recu : '%s'\n", input);
      }
      else
      {
        break;
      }
    } // End while
  }

  return 0;
} // main


Le programme consiste en deux processus. Le premier (le père) lit une ligne de caractères saisie au clavier et l'envoie sur la partie maître du pty. Le second (le fils), a fait du pty esclave, son entrée et ses sorties standards et renvoie sur sa sortie standard, toute chaîne lue, préfixée par « Le fils a recu : ». Voici un exemple de lancement :

$ ./mypty2
Entree : azerty
Le fils a recu : 'azerty'
Entree : qwerty
Le fils a recu : 'qwerty'
Entree : pwd
Le fils a recu : 'pwd'

La figure 3 explique le fonctionnement de mypty2 lorsque l'opérateur saisit la chaîne de caractères « qwerty » :

Figure 3 : Fonctionnement de mypty2

figure_3

Côté processus fils, on remarquera la configuration du pty esclave via les appels à cfmakeraw() et tcsetattr() de sorte à passer en mode raw (brut en français) pour désactiver les opérations telles que l'écho.
On peut rendre plus générique la modification faite dans mypty2 pour donner la possibilité d'exécuter n'importe quel programme derrière le pty. Dans mypty3, le processus père envoie tout ce qui vient de son entrée standard sur le pty maître et tout ce qui vient du pty maître sur sa sortie standard. Il se conduit simplement comme un relayeur de données. Le processus fils effectue les mêmes opérations que précédemment mais se généralise pour éxécuter un programme interactif quelconque avec ses paramètres passés en arguments. Nous noterons les appels à setsid() et ioctl(TIOCSCTTY) pour faire en sorte que le pty esclave soit le terminal de contrôle du programme exécuté. On notera aussi la fermeture du descripteur de fichier fds qui n'est plus utile après les appels à 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];

  // Contrôle des arguments
  if (ac <= 1)
  {
    fprintf(stderr, "Usage: %s nom_de_programme [parametres]\n", av[0]);
    exit(1);
  }

  fdm = posix_openpt(O_RDWR);
  if (fdm < 0)
  {
    fprintf(stderr, "Erreur %d sur posix_openpt()\n", errno);
    return 1;
  }

  rc = grantpt(fdm);
  if (rc != 0)
  {
    fprintf(stderr, "Erreur %d sur grantpt()\n", errno);
    return 1;
  }

  rc = unlockpt(fdm);
  if (rc != 0)
  {
    fprintf(stderr, "Erreur %d sur unlockpt()\n", errno);
    return 1;
  }

  // Ouverture du PTY esclave
  fds = open(ptsname(fdm), O_RDWR);

  // Création d'un processus fils
  if (fork())
  {
  fd_set fd_in;

    // Code du processus pere

    // Fermeture de la partie esclave du PTY
    close(fds);

    while (1)
    {
      // Attente de données de l'entrée standard et du PTY maître
      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, "Erreur %d sur select()\n", errno);
                  exit(1);

        default :
        {
          // S'il y a des donnees sur l'entree standard
          if (FD_ISSET(0, &fd_in))
          {
            rc = read(0, input, sizeof(input));
            if (rc > 0)
            {
              // Envoie des données sur le PTY maitre
              write(fdm, input, rc);
            }
            else
            {
              if (rc < 0)
              {
                fprintf(stderr, "Erreur %d sur read entree standard\n", errno);
                exit(1);
              }
            }
          }

          // S'il y a des donnees sur le PTY maitre
          if (FD_ISSET(fdm, &fd_in))
          {
            rc = read(fdm, input, sizeof(input));
            if (rc > 0)
            {
              // Envoie des données sur la sortie standard
              write(1, input, rc);
            }
            else
            {
              if (rc < 0)
              {
                fprintf(stderr, "Erreur %d sur read PTY maitre\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

    // Code du processus fils

    // Fermeture de la partie maitre du PTY
    close(fdm);

    // Sauvegarde des parametre par defaut du PTY esclave
    rc = tcgetattr(fds, &slave_orig_term_settings);

    // Positionnement du PTY esclave en mode RAW
    new_term_settings = slave_orig_term_settings;
    cfmakeraw (&new_term_settings);
    tcsetattr (fds, TCSANOW, &new_term_settings);

    // Le cote esclave du PTY devient l'entree et les sorties standards du fils

    // Fermeture de l'entrée standard (terminal courant)
    close(0);

    // Fermeture de la sortie standard (terminal courant)
    close(1);

    // Fermeture de la sortie erreur standard (terminal courant)
    close(2);

    // Le PTY devient l'entree standard (0)
    dup(fds);

    // Le PTY devient la sortie standard (1)
    dup(fds);

    // Le PTY devient la sortie erreur standard (2)
    dup(fds);

    // Maintenant le descripteur de fichier original n'est plus utile
    close(fds);

    // Le process courant devient un leader de session
    setsid();

    // Comme le process courant est un leader de session, La partie esclave du PTY devient sont terminal de contrôle
    // (Obligatoire pour les programmes comme le shell pour faire en sorte qu'ils gerent correctement leurs sorties)
    ioctl(0, TIOCSCTTY, 1);

    // Execution du programme
   {
   char **child_av;
   int i;

     // Construction de la ligne de commande
     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);
  }

  return 0;
} // main


Ci-après, on lance mypty3 avec la calculatrice bc à travers le pty :

$ ./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 sur read PTY maitre
$

Il est possible de lancer un shell ou tout autre programme interactif à la place de bc. Cette technique s'appliquent à nombre de programmes célèbres tels que xterm, telnet, ssh, rlogin, rsh... Par exemple, la figure 4 décrit l'architecture de telnet.

Figure 4 : Description d'une session telnet

figure_4


Pour faire le parallèle avec les programmes d'exemples précédents, le démon telnetd est le processus père dont l'entrée standard n'est pas un simple terminal mais un terminal déporté à travers un réseau où s'éxécute le client telnet. Le processus fils est un shell bash. Tout ce qui provient du client telnet à travers le réseau est transmis par telnetd sur le pseudo-terminal maître. Tout ce qui provient du shell bash à travers le pseudo-terminal, est transmis au client telnet par le réseau.

4.2 Limitation de grantpt()

Si on lit attentivement le manuel en ligne de grantpt(), il est dit que son comportement est non spécifié si un handler de signal est installé pour capturer le signal SIGCHLD. La raison se trouve dans le code source de grantpt() dans la GLIBC. Cette dernière peut se terminer avec un appel à fork() pour exécuter le programme chown tandis que le père (i.e. l'appelant de grantpt()) attend sa terminaison avec waitpid(). D'où l'impossibilité de capturer le signal SIGCHLD sinon l'appel système waitpid() dans grantpt() échouera. Voici un extrait du code source de la fin de 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;
[...]


Il faut donc désactiver tout handler de signal sur SIGCHLD avant appel à grantpt() et le rétablir juste après.

4.3 Prise de contrôle d'un processus intéractif

mypty3 peut être modifié pour rendre le processus père plus intelligent de sorte à lui faire interpréter un scénario avec un ensemble de commandes qui lui permettent de se synchroniser avec le processus fils. En d'autres termes, on pourrait remplacer l'opérateur humain par un script de commandes. C'est tout simplement ce qui est fait par le programme pdip que nous voyons dans la suite.

5. Présentation de PDIP

pdip est l'acronyme de « Programmed Dialogue with Interactive Programs ». En français, cela donne « Dialogue Programmé avec des Programmes Interactifs ». Ce nom provient des premières lignes du manuel du programme expect dont pdip se veut être une version extrêmement simplifiée. C'est un utilitaire qui accepte un scénario en entrée pour dialoguer avec un programme interactif. pdip est disponible en source libre sur sourceforge.

Comme expect, pdip accepte un langage de commande pour envoyer et recevoir des chaînes de caractères à un programme. Mais contrairement à expect, le langage de commande n'est pas évolué au point d'accepter des structures de contrôle de haut niveau ou des branchements. Il ne permet pas non plus de dialoguer avec plusieurs programmes en même temps ou de rendre la main à l'opérateur en cours de session.

Comme indiqué en figure 5, pdip reçoit en paramètre le chemin d'accès du programme intéractif à exécuter et pour interagir avec, il accepte un langage de commandes simples sur son entrée standard ou sous forme d'un script passé en paramètre.

Figure 5 : Vue générale du fonctionnement de pdip

figure_5


La liste des commandes disponibles pouvant être interprétées par pdip est :

Début de commentaire
Positionne à x secondes le temps maximum à attendre après chacune des commandes qui suivent (la valeur 0 annulle le temporisateur, c’est la comportement par défaut).
Attend une chaîne de cractères venant du programme se conformant au modèle w1 w2... Le modèle est une expression régulière conforme à regex.
Envoie la chaîne de caractères w1 w2... au programme. La chaîne peut contenir les caractères de contrôle suivants:
Envoie  le  signal  Linux  signame au programme.  signame peut prendre les valeurs : HUP, INT, QUIT, ILL, TRAP, ABRT, BUS, FPE, KILL, USR1, SEGV, USR2, PIPE, ALRM, TERM.
Arrête toute activité pendant x secondes
Fin de session

6. Utilisation de PDIP

Utiliser pdip est d'une simplicité extrême comme on le voit ici avec le pilotage d'un client telnet qui se connecte à une machine appelée « remote » avec le nom de login « foo » et le mot de passe « bar ». Ensuite la commande ls est exécutée avant de terminer la session :

$ pdip --cmd=’telnet remote’
recv "login" # Attente du prompt « login: »
send "foo\n" # Envoi du nom de login ’foo’
recv "Password"# Attente du prompt « Password »
send "bar\n" # Envoi du mot de passe ’bar’
recv "\$ " # Attente du prompt du shell « $ »
send "ls\n" # Lancement de la commande ’ls’
recv "\$ " # Attente du prompt du shell « $ »
send "exit\n" # Sortie du shell
exit # Sortie de PDIP
$


Le script est largement commenté. On précisera toutefois que étant donné que la commande « recv », reçoit une expression régulière en paramètre, il convient de ne pas oublier d'inhiber les caractères spéciaux tels que $ à l'aide du caractère \. D'où la commande recv "\$ " pour attendre le prompt du shell. Voici un exemple de résultat de ce script pdip :

$ pdip --cmd='telnet remote'
recv "login »
Trying 192.0.1.12...
Connected to remote.
Escape character is '^]'.

Linux 2.6.22-14-generic (remote) (pts/10)
remote loginsend "foo\n"
recv "Password"

: foo
Passwordsend "bar\n"
recv "\$ "

:
Last login: Tue Nov 6 20:06:51 CET 2007 on :0
Linux remote 2.6.22-14-generic #1 SMP Sun Oct 14 23:05:12 GMT 2007 i686

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.
foo@remote:~$ send "ls\n"
recv "\$ "

ls
DIR2 DOCUMENTS PERSO TODO
Applications PHOTOS VIDEOS
foo@remote:~$ send "exit\n"
exit


Conclusion

La notion de pseudo-terminal est très largement utilisé dans le monde Unix en général à travers les utilitaires les plus célèbres tels que telnet ou xterm. Cet article a présenté l'une de ses applications à travers pdip qui a pour but de piloter des programmes interactifs.

Notes 

[1] man 7 pty

[2] man 7 regex
[3] Programmed Dialogues with Interactive Programs (PDIP)
[4] Utilisation des pseudo-terminaux pour piloter les programmes interactifs - glmf_logo
[5] lpty - PTY control for Lua

A propos de l'auteur

L'auteur est un ingénieur en informatique travaillant en France. Il peut être contacté à ici ou vous pouvez consulter son site WEB.


Page principale
  Page précédente