Nous avons tout pour que je puisse vous proposer le code suivant, qui exécute un exécutable que j'appellerai greffon, récupère tout ce qu'il écrit sur ses entrées standards et d'erreur, et qui récupère aussi son code de sortie. Voici le code:
1 #define _USE_BSD 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 #include <sys/types.h> 6 #include <sys/select.h> 7 #include <sys/time.h> 8 #include <sys/resource.h> 9 #include <sys/wait.h> 10 #include <errno.h> 11 #include <sys/stat.h> 12 #include <fcntl.h> 13 14 int 15 main (int argc, char *argv[]) 16 { 17 int flux_in, flux_out, flux_err; 18 int real_stdin, real_stdout, real_stderr; 19 int p[2]; 20 int pid; 21 int c; 22 fd_set readfds; 23 int retval; 24 int status; 25 int retpid; 26 int plugin_has_quit = 0; 27 28 real_stdin = dup (STDIN_FILENO); 29 real_stdout = dup (STDOUT_FILENO); 30 real_stderr = dup (STDERR_FILENO); 31 32 printf ("OK\n"); 33 34 if (pipe (p)) 35 { 36 printf ("Erreur a la creation du pipe\n"); 37 exit (EXIT_FAILURE); 38 } 39 flux_in = p[1]; 40 close (STDIN_FILENO); 41 dup (p[0]); 42 close (p[0]); 43 44 if (pipe (p)) 45 { 46 printf ("Erreur a la creation du pipe\n"); 47 exit (EXIT_FAILURE); 48 } 49 flux_out = p[0]; 50 close (STDOUT_FILENO); 51 dup (p[1]); 52 close (p[1]); 53 54 if (pipe (p)) 55 { 56 printf ("Erreur a la creation du pipe\n"); 57 exit (EXIT_FAILURE); 58 } 59 flux_err = p[0]; 60 close (STDERR_FILENO); 61 dup (p[1]); 62 close (p[1]); 63 64 switch (pid = fork ()) 65 { 66 case 0: 67 printf ("test\n"); 68 execlp ("/tmp/plugin", "plugin", NULL); 69 break; 70 case -1: 71 printf ("Erreur lors du fork()\n"); 72 exit (EXIT_FAILURE); 73 } 74 75 close (STDIN_FILENO); 76 dup (real_stdin); 77 close (STDOUT_FILENO); 78 dup (real_stdout); 79 close (STDERR_FILENO); 80 dup (real_stderr); 81 82 FD_ZERO (&readfds); 83 FD_SET (flux_out, &readfds); 84 FD_SET (flux_err, &readfds); 85 86 retval = 87 select ((flux_err > flux_out) ? flux_err + 1 : flux_out + 1, &readfds, 88 NULL, NULL, NULL); 89 retpid = wait4 (pid, &status, WNOHANG, NULL); 90 while ((!plugin_has_quit) || (retpid < 0)) 91 { 92 if (retval > 0) 93 { 94 if (flux_out >= 0) 95 { 96 if (FD_ISSET (flux_out, &readfds)) 97 { 98 int r; 99 if ((r = read (flux_out, &c, sizeof (char))) == -1) 100 printf ("retval=%d errno=%d\n", retval, errno); 101 else if (r == sizeof (char)) 102 printf ("%c", c); 103 else if (r == 0) 104 flux_out = -1; 105 } 106 } 107 if (flux_err >= 0) 108 { 109 if (FD_ISSET (flux_err, &readfds)) 110 { 111 int r; 112 if ((r = read (flux_err, &c, sizeof (char))) == -1) 113 printf ("retval=%d errno=%d\n", retval, errno); 114 else if (r == sizeof (char)) 115 printf ("%c", c); 116 else if (r == 0) 117 flux_err = -1; 118 } 119 } 120 if (!plugin_has_quit) 121 { 122 FD_ZERO (&readfds); 123 if (flux_out >= 0) 124 FD_SET (flux_out, &readfds); 125 if (flux_err >= 0) 126 FD_SET (flux_err, &readfds); 127 if ((flux_out == -1) && (flux_err == -1)) 128 plugin_has_quit = 1; 129 else 130 retval = 131 select ((flux_err > flux_out) ? flux_err + 1 : flux_out + 1, 132 &readfds, NULL, NULL, NULL); 133 } 134 else 135 { 136 retval = 0; 137 sleep (1); 138 } 139 if (retpid < 0) 140 retpid = wait4 (pid, &status, WNOHANG, NULL); 141 } 142 } 143 printf ("\nStatus de retour: %d\n", WEXITSTATUS (status)); 144 145 exit (EXIT_SUCCESS); 146 }
Si vous voulez un greffon de test, en voici le listing au cas où vous auriez une panne d'imagination. Appelons l'exécutable généré par ce code du nom très original plugin et plaçons-le dans le répertoire /tmp:
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int 5 main (int argc, char *argv[]) 6 { 7 printf ("resultat\n"); 8 fprintf (stderr, "erreur?\n"); 9 exit (4); 10 }
Lignes 28, 29 et 30, je sauvegarde l'entrée standard, la sortie standard et la sortie d'erreur standard. En effet, il me faudra faire du bricolage par la suite, et le chargeur aura besoin de revenir à l'état de départ. Notez qu'il y a au moins deux autres manières de faire. L'une est de faire le bricolage qui suit uniquement pour le processus fils, sans toucher au père (c'est plus propre et je vous le conseille, mais ca m'empèche de vous montrer comment on fait pour sauvegarder les flux standards). Une autre est de ne rien sauvegarder du tout, et de réouvrir /dev/tty
avec les options qui vont bien des que l'on en a de nouveau besoin. La restauration de ces flux a lieu lignes 75 à 80.
Lignes 34 à 42, je crée un tube, et en fermant l'entrée standard, je simule celle-ci par la suite. C'est exactement ce que j'ai décrit plus haut dans le paragraphe Manipulation des descripteurs de fichiers. Pas besoin de commentaires supplémentaires: vous avez le droit de relire le paragraphe en question - ou voici un artifice pour faire durer plus longtemps le plaisir de la lecture du magazine sans en écrire plus -
Lignes 44 à 52 et 54 à 62, le principe est le même que dans le bloc précédent, à ceci près que nous traitons la sortie standard puis la sortie d'erreur standard. De plus, ce sont des sorties et pas des entrées: il faut inverser p[0]
et p[1]
.
Lignes 64 à 73, nous avons le fork()
avec en ligne 68 le lancement du greffon via execlp()
. Ceci était l'objet du début de l'article: vous en avez l'exemple ici.
A partir de la ligne 82, vous avez du code qui n'est exécuté que par le père, et qui consiste à lire tout ce que le greffon peut vouloir afficher, ainsi que la lecture du code d'erreur. Dois-je revenir sur cette partie qui est en fait un appel à la fonction select()
avec les arguments qu'elle demande, puis la gestion de ce qu'elle retourne? Référez-vous à l'opus numéro 8 des briques en C qui traite justement de cette fonction, en long en large et en travers. Par contre, notez que si read()
renvoie zéro, c'est que le descripteur de fichier a été fermé en face - il n'y a plus rien à lire -. Je mets donc de descripteur de fichier à -1, et lorsque les deux descripteurs de fichiers qui m'interessent sont à -1, je suis prêt à quitter la boucle principale. Prêt? Presque, car il faut encore aussi que wait4
m'ait renvoyé le status de retour du greffon, lorsque celui-ci a fini son exécution.
Enfin, je voudrais attirer votre attention sur la ligne 137 et l'appel à sleep(1)
. Sans cet appel, vous pouvez avoir un bug qui n'apparaît que dans les applications parallèles. Or l'exécution d'un greffon parallèlement à son chargeur n'est rien d'autre qu'une application parallèle! Quant au bug, il est que vous avez une boucle principale dans le chargeur, et que s'il n'y a plus rien a lire, la boucle va tourner à vide, avec pour seul appel celui à wait4()
. Le risque est que, dans une telle boucle, le processeur soit monopolisé par la boucle et ne puisse plus exécuter quoi que ce soit d'autre. l'appel à wait4()
permet de redonner la main au système un court instant. Comme cet appel est non bloquant, le chargeur risque donc de monopoliser le processeur pour ne rien faire, à attendre que le greffon finisse, mais en l'empechant de finir par cette monopolisation du processeur! L'appel à sleep()
permet donc de redonner la main au système et de ne pas monopoliser le processeur. Notez cependant que dans l'exemple que je vous ai donné, ce cas a peu de chances de se matérialiser quand même. Mais souvenez-vous en pour plus tard!
© 2002 Yves Mettier