Question FAQ : J’essaye de lancer un programme par exec() depuis mon serveur et de lui attacher la socket mais je ne reçoit pas toutes les données. Pourquoi ?

Si le programme que vous lancez utilise printf() (et autres flux definis dans « stdio.h »), vous devez gérer 2 buffers. Le noyau stocke les entrées/sorties du socket dans un buffer. Le deuxième buffer est celui qui cause vos problèmes, c’est le buffer des entrées/sorties géré par « stdio ».
La réponse courte à ce problème est que vous tentez d’utiliser un pty plutôt qu’un socket pour vos entrées/sorties. Le reste de cette réponse est une tentative pour expliquer pourquoi.
D’abord, le buffer de la socket contrôlé par setsockopt() n’a strictement rien à voir avec le buffer « stdio ». Mettre ce buffer à 1 octet est garanti pour être la mauvaise méthode.
Le diagramme suivant devrait expliquer les choses un peu mieux.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
        Process A                   Process B
    +---------------------+     +---------------------+
    |                     |     |                     |
    |    mainline code    |     |    mainline code    |
    |         |           |     |         ^           |
    |         v           |     |         |           |
    |      fputc()        |     |      fgetc()        |
    |         |           |     |         ^           |
    |         v           |     |         |           |
    |    +-----------+    |     |    +-----------+    |
    |    | stdio     |    |     |    | stdio     |    |
    |    | buffer    |    |     |    | buffer    |    |
    |    +-----------+    |     |    +-----------+    |
    |         |           |     |         ^           |
    |         |           |     |         |           |
    |      write()        |     |       read()        |
    |         |           |     |         |           |
    +-------- | ----------+     +-------- | ----------+
              |                           |                  User space
  ------------|-------------------------- | ---------------------------
              |                           |                Kernel space
              v                           |
         +-----------+               +-----------+
         | socket    |               | socket    |
         | buffer    |               | buffer    |
         +-----------+               +-----------+
              |                           ^
              v                           |
      (AF- and protocol-          (AF- and protocol-
       dependent code)             dependent code)
En supposant que les deux processus communiquent entre eux (les mécanismes de communication ont été délibérément omis, ce n’est pas le cœur du problème), vous pouvez voir que les données écrites par le processus A dans son buffer « stdio » sont complètement inaccessibles au processus B. C’est uniquement lorsque la décision est prise de vider ce buffer (par un write()) que les données peuvent être délivrées à l’autre processus.
La seule manière de changer cette manière de faire à l’intérieur du processus A est de changer le code. Cependant, la politique de bufferisation pour « stdout » est contrôlée par le fait que le descripteur de fichier est un terminal (pty) ou non. Généralement, les sorties sur un terminal sont bufferisées par ligne et les sorties sur des non-terminaux (fichiers, pipes, sockets, périphériques non tty, …) est complètement bufferisée. Donc l’effet désiré peut habituellement être atteint en utilisant un périphérique de type terminal (pty).
Comme le buffer « stdio » (et les structures FILE et tout ce qui est relatif à « stdio ») est situé dans l’espace mémoire utilisateur, cet espace n’est pas préservé par l’appel exec(), c’est pourquoi appeler setvbuf() avant exec() n’est pas efficace.
Une solution est décrite dans le programme suivant.
Ce que fait ce programme est d’attendre une connexion sur un port et ensuite, il appelle la fonction forkpty(). Cette fonction retourne une valeur différente pour différencier le processus père du fils. Après cet appel, le fils fonctionne dans un pty.
Dans ce programme, le fils appelle tout simplement exec() pendant que le père utilise read() et write() dans une boucle qui copie tout ce qui arrive du pty dans la socket (et vice versa).
Il manqué la gestion des signaux SIGCHLD et SIGPIPE, le programme fait ses entrées/sorties un octet à la fois et il ne supporte qu’une seule connexion mais cela devrait donner une idée.
A noter que tous les systèmes n’ont pas la fonction forkpty() dans les mêmes headers ni dans la même bibliothèque. Solaris ne possède pas de fonction forkpty()
De plus, vous risquez d’avoir des soucis si vous utilisez le client telnet pour tester ce programme. Cela marcherait probablement mieux si vous utilisiez un simple client TCP en mode RAW.
Le fichier pty_serv.c
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <errno.h> 
#include <sys/types.h> 
#include <sys/socket.h> 
#include <netinet/in.h> 

/* 
 * forkpty may be in diferent places 
 * depending of your OS 
 */ 

#ifdef LINUX /* You need to link with libutil */ 
#include <pty.h> 
#endif 
   
#ifdef FREEBSD /* You need to link with libutil */ 
#include <libutil.h> 
#endif 

#ifdef MACOSX /* Aparently no link flags are needed */ 
#include <stdint.h> 
#include <util.h> 
#endif 

#ifdef SOLARIS /* Use the code in my_forkpty.c */ 
int my_forkpty (int *amaster, char *name, void *unused1, void *unused2); 
#define forkpty my_forkpty 
#endif 

#define EXEC_FILE "./test" 

#define PORT 1234 

int 
get_connect_from (uint16_t port) 
{ 
  int srv_sock, client_sock; 
  struct sockaddr_in srv_name, client_name; 
  int size; 

  srv_sock = socket (PF_INET, SOCK_STREAM, 0); 
  if (srv_sock < 0) 
    { 
      perror ("socket()"); 
      exit (EXIT_FAILURE); 
    } 

  srv_name.sin_family = AF_INET; 
  srv_name.sin_port = htons (port); 
  srv_name.sin_addr.s_addr = htonl (INADDR_ANY); 
  if (bind (srv_sock, (struct sockaddr *) &srv_name, sizeof (srv_name)) < 0) 
    { 
      perror ("bind()"); 
      exit (EXIT_FAILURE); 
    } 

  if (listen (srv_sock, 1) < 0) 
    { 
      perror ("listen()"); 
      exit (EXIT_FAILURE); 
    } 

  size = sizeof(client_name); 
  client_sock = accept (srv_sock, (struct sockaddr *) &client_name, &size); 
  if (client_sock < 0) 
    { 
      perror ("accept"); 
      exit (EXIT_FAILURE); 
    } 

  return client_sock; 
} 

int 
main (void) 
{ 
  int sock; 
  fd_set descriptor_set; 
  char c; 

  int pty; 
   
  sock = get_connect_from(PORT); 
   
  switch (forkpty (&pty, /* pseudo-terminal master end descriptor */ 
                   NULL, /* This can be a char[] buffer used to get... */ 
                              /* ...the device name of the pseudo-terminal */ 
                   NULL, /* This can be a struct termios pointer used... */ 
                              /* to set the terminal attributes */ 
                   NULL)) /* This can be a struct winsize pointer used... */ 
    { /* ...to set the screen size of the terminal */ 
    case -1: /* Error */ 
      perror ("fork()"); 
      exit (EXIT_FAILURE); 

    case 0: /* This is the child process */ 
      close (sock); 
      execl(EXEC_FILE, EXEC_FILE, NULL); 

      perror("exec()"); /* Since exec* never return */ 
      exit (EXIT_FAILURE); 

    default: /* This is the parent process */ 
      while (1) 
        { 
          FD_ZERO (&descriptor_set); 
          FD_SET (sock, &descriptor_set); 
          FD_SET (pty, &descriptor_set); 
           
          if (select (FD_SETSIZE, &descriptor_set, NULL, NULL, NULL) < 0) 
            { 
              perror ("select()"); 
              exit (EXIT_FAILURE); 
            } 
           
          if (FD_ISSET (sock, &descriptor_set)) 
            { 
              if ( (read (sock, &c, 1) != 1) 
                   || (write (pty, &c, 1) != 1) ) 
                { 
                  fprintf (stderr, "Disconnected\n"); 
                  exit (EXIT_FAILURE); 
                } 
            } 
             
          if (FD_ISSET (pty, &descriptor_set)) 
            { 
              if ((read (pty, &c, 1) != 1) 
                  || (write (sock, &c, 1) != 1) ) 
                { 
                  fprintf (stderr, "Disconnected\n"); 
                  exit (EXIT_FAILURE); 
                } 
            } 
        } 
    } 
     
  return EXIT_FAILURE; 
}
Le fichier my_forkpty.c
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <fcntl.h> 
#include <unistd.h> 
#include <stdlib.h> 
#include <sys/ioctl.h> 
#include <sys/stream.h> 
#include <sys/stropts.h> 

/* fork_pty() remplacement for Solaris. 
 * This ignore the last two arguments 
 * for the moment 
 */ 
int 
my_forkpty (int *amaster, 
            char *name, 
            void *unused1, 
            void *unused2) 
{ 
  int master, slave; 
  char *slave_name; 
  pid_t pid; 
   
  master = open("/dev/ptmx", O_RDWR); 
  if (master < 0) 
    return -1; 

  if (grantpt (master) < 0) 
    { 
      close (master); 
      return -1; 
    } 

  if (unlockpt (master) < 0) 
    { 
      close (master); 
      return -1; 
    } 

  slave_name = ptsname (master); 
  if (slave_name == NULL) 
    { 
      close (master); 
      return -1; 
    } 

  slave = open (slave_name, O_RDWR); 
  if (slave < 0) 
    { 
      close (master); 
      return -1; 
    } 

  if (ioctl (slave, I_PUSH, "ptem") < 0 
      || ioctl (slave, I_PUSH, "ldterm") < 0) 
    { 
      close (slave); 
      close (master); 
      return -1; 
    } 

  if (amaster) 
    *amaster = master; 

  if (name) 
    strcpy (name, slave_name); 
   
  pid = fork (); 
  switch (pid) 
    { 
    case -1: /* Error */ 
      return -1; 
    case 0: /* Child */ 
      close (master); 
      dup2 (slave, STDIN_FILENO); 
      dup2 (slave, STDOUT_FILENO); 
      dup2 (slave, STDERR_FILENO); 
      return 0; 
    default: /* Parent */ 
      close (slave); 
      return pid; 
    } 

  return -1; 
}