Uso Avanzado de Seudoterminales

El siguiente ejemplo muestra como podemos hacer que el proceso a cargo del lado esclavo sea quien controle la terminal y como obtener el tamaño de la ventana de la terminal. El programa crea un proceso hijo (línea 14) y establece una comunicación a través de la seudoterminal (creada en la línea 8) entre padre e hijo. El hijo ejecuta el comando del sistema (línea 33) pasado en la línea de comandos @ARGV.

En las líneas 11-13 se crea un canal pipe que será usado para que el padre obtenga el estatus de retorno del hijo (línea 70). Si el exec de la línea 33 fracasa el hijo envía el código de error. Si no fracasa, el exec produce el cierre del pipe y el padre recibe el end-of-file.

Ejercicio 7.1.4   ¿Por que no usar un wait como es habitual para obtener el estatus de retorno?

En la línea 18 el proceso hijo establece el manejador de ficheros del lado esclavo como la terminal de control y hace que el proceso hijo se convierta en el lider de sesión.

En la línea 21 mediante la llamada $slave->clone_winsize_from(\*STDIN) se obtiene el tamaño de la seudoterminal asociada con el manejador de ficheros STDIN y se transfiere al pty. Nótese que debe ser llamado a través del lado esclavo (líneas 21 y 40).

Después de hacer que STDIN, STDOUT y STDERR estén asociados con el lado esclavo de la terminal en las líneas 24-29 se procede a ejecutar el comando. Las llaves alrededor del exec evitan Perl envíe un mensaje de warning.

El proceso padre dispone el manejador para la señal de cambio de tamaño de la ventana WINCH (línea 77). Abre un fichero de log en el que se volcará la sesión (línea 78).

Después el padre permanece en un bucle a la escucha de los manejadores asociados con la entrada estandar (STDIN) y con la seudoterminal ($pty). Cada entrada es volcada mediante la subrutina redirect en el fichero de log y en el manejador de salida complementario del de la entrada que se acaba de producir. Se vuelca en $pty si la entrada viene de $pty y en STDOUT si la entrada viene de STDIN. Para ello se usa el objeto $rs de la clase IO::Select. Si los dos manejadores de entrada $pty y STDIN están listos (@ready >1) se le da siempre prioridad a la entrada que viene de $pty (línea 86). El eco de la entrada desde STDIN también se retrasa si la terminal no esta lista para escritura (!$ws->can_write(TIMEOUT), véase la línea 91). Para ello se usa el otro objeto $ws de la clase IO::Select construido en la línea 80.

lhp@nereida:~/Lperl/src/perl_networking/ch2$ cat -n try7
 1  #!/usr/bin/perl -w
 2  use strict;
 3  use IO::Pty;
 4  use IO::Select;
 5  use IO::Handle;
 6  use constant TIMEOUT=>3600;
 7
 8  my $pty = new IO::Pty;
 9  my $pid;
10
11  pipe(STAT_RDR, STAT_WTR)
12    or die "Cannot open pipe: $!";
13  STAT_WTR->autoflush(1);
14  $pid = fork();
15  die "Cannot fork" if not defined $pid;
16  unless ($pid) { # Child
17    close STAT_RDR;
18    $pty->make_slave_controlling_terminal();
19    my $slave = $pty->slave();
20    close $pty;
21    $slave->clone_winsize_from(\*STDIN);
22    $slave->set_raw();
23
24    STDIN->fdopen($slave, "<")
25      or die "Can't reopen STDIN for reading, $!\n";
26    STDOUT->fdopen($slave, ">")
27      or die "Can't reopen STDOUT for writing, $!\n";
28    STDERR->fdopen($slave, ">")
29      or die "Can't reopen STDERR for writing, $!\n";
30
31    close $slave;
32
33    { exec(@ARGV) }; # exec produce un close de los ficheros abiertos
34    print STAT_WTR $!+0; # Fuerza contexto numérico
35    die "Cannot exec(@ARGV): $!";
36  }
37  &parent();
38
39  sub winch {
40    $pty->slave->clone_winsize_from(\*STDIN);
41    kill WINCH => $pid if $pid; # Si soy el padre envio la señal
42    print "STDIN terminal size changed.\n";
43    $SIG{WINCH} = \&winch; # En ciertos S.O.
44  }
45
46  sub redirect {
47   my ($src,$dst) = @_;
48   my $buf = '';
49   my $read = sysread($src, $buf, 1);
50   if (defined $read && $read) {
51     syswrite($dst,$buf,$read);
52     syswrite(LOG,$buf,$read);
53     return 0;
54    }
55   else { # EOF
56     print STDERR "Nothing from $src";
57     print "$read\n" if defined($read);
58     return 1
59    }
60  }
61
62  sub parent {
63    $pty->autoflush(1);
64    close STAT_WTR;
65    $pty->close_slave();
66    $pty->set_raw();
67    # Esperamos por el comienzo de la ejecución del exec
68    # eof debido al exec o error
69    my $errno;
70    my $errstatus = sysread(STAT_RDR, $errno, 256);
71    die "Cannot sync with child: $!" if not defined $errstatus;
72    close STAT_RDR;
73    if ($errstatus) {
74      $! = $errno+0;
75      die "Cannot exec(@ARGV): $!";
76    }
77    $SIG{WINCH} = \&winch;
78    open(LOG,">log") || die;
79    STDOUT->autoflush(1);
80    my ($rs, $ws) = (IO::Select->new(), IO::Select->new());
81    $rs->add(\*STDIN, $pty);
82    $ws->add($pty);
83    { # infinite loop
84      my @ready = $rs->can_read(TIMEOUT);
85      if (@ready) {
86        @ready = reverse @ready if (@ready >1) and ($ready[0] != $pty);
87        if ($ready[0] == $pty) {
88          my $eof = redirect($pty, \*STDOUT);
89          last if $eof;
90        }
91        elsif ($ws->can_write(TIMEOUT)) { # Algo en STDIN
92          my $eof = redirect(\*STDIN, $pty);
93          last if $eof;
94        }
95      }
96      redo;
97    }
98    close(LOG);
99  }

Ejercicio 7.1.5   Ejecute el programa anterior en una xterm con argument bash:
try7 bash
  1. Cambie el tamaño de la ventana con el ratón. Ejecute stty size para los diferentes tamaños. ¿Que ocurre?
  2. Cambie el tamaño de la ventana con el ratón y ejecute ls para diversos tamaños. ¿Que observa? ¿Se adapta la salida de ls al tamaño de la ventana?
  3. Modifique el programa para que cuando se cambie el tamaño de la ventana se escriba su nuevo tamaño (alto, ancho). Use la función GetTerminalSize en Term::ReadKey o bien repase la sección 7.1.3.
  4. Suprima las líneas que ponen la seudoterminal en modo raw. Observe que ocurre en el fichero de log. ¿Cómo cambia?
  5. ¿Quién esta ejecutando el manejador para la señal WINCH, el padre, el hijo o ambos?
  6. ¿Que ocurre si se suprime la línea en la que el hijo llama a $pty->make_slave_controlling_terminal()? ¿Que significa el mensaje que se produce?
  7. Ejecute el programa con el depurador en una xterm.
  8. Ejecute de nuevo el programa, pero esta vez con argumento vi tutu (el editor). ¿Que ocurre?
  9. Ejecute de nuevo try7 vi tutu pero antes de hacerlo ponga la xterm en modo cbreak. ¿Como cambia la conducta?
  10. Ejecute de nuevo try7 vi pero antes de hacerlo ponga la xterm en modos cbreak y -echo. ¿Como cambia? ¿Funciona?
  11. Reescriba el bucle final de lectura-escritura en try7 para que las entradas desde teclado se hagan sin eco en STDOUT y sin esperas. Ejecute la nueva versión try8 con el comando try8 vi tutu. Puede usar Term::ReadKey. Lea la documentación de Term::ReadKey relativa a las funciones ReadKeyy ReadMode. Preste antención a los modos cbreak y normal. ¿Como cambia la conducta?
  12. Ejecute de nuevo el programa esta vez con un telnet o una ssh.

Casiano Rodríguez León
Licencia de Creative Commons
Programación Distribuida y Mejora del Rendimiento
por Casiano Rodríguez León is licensed under a Creative Commons Reconocimiento 3.0 Unported License.

Permissions beyond the scope of this license may be available at http://campusvirtual.ull.es/ocw/course/view.php?id=44.
2012-06-19