El inicio de un Terminal, con Linux y Free Pascal

RESUMEN

Se explicará como implementar un emulador de terminal en Linux, usando Free Pascal, para ejecutar un proceso cualquiera, y poder comunicarse con él.

INTRODUCCIÓN

En los tiempos actuales, de interfaces gráficas y pantallas sensibles al tacto, alguien podría pensar que los terminales (o «pantallas negras», para algunos) son parte del registro arqueológico de la computación.

Nada más lejos de la realidad. Los terminales o «consolas», siguen siendo de uso común en nuestros días, sea en su forma visual para el usuario, o para el trabajo interno de casi todos los sistemas operativos actuales.

No voy a entrar en detalles de lo que se entiende por terminal, consola, terminal virtual, pseudo terminal, etc. Esos detalles se pueden encontra fácilmente por la web. Doy por sentado también, que se conocen los detalles básicos del sistema operativo Linux, especialmente en lo referido a procesos , al mecanismo de tuberías, y a los flujos stdin, stdout y stderr.

En este artículo, voy a explicar como implementar un terminal en linux, usando Free Pascal, para ejecutar un proceso cualquiera, y poder comunicarse con él.

La motivación de este trabajo, es poder automatizar tareas interactuando con procesos, a través de un terminal, en lugar de los flujos «stdin», «stdout» y «sdterr», ya que algunos procesos interactuan directamente con un terminal, especialmente aquellos que solicitan contraseñas.

Este trabajo está basado en el excelente artículo http://rachid.koucha.free.fr/tech_corner/pty_pdip.html de R. Koucha.

Lo que queremos implementar en realidad sería un emulador de terminal, ya que un verdadero terminal es un elemento electrónico o mecánico, que se conecta remotamente a un servidor, como un hardware adicional.

Lo que cotidianamente usamos, como terminal remoto (Putty, Teraterm, Kitty, … ), o local (como el GNOME Terminal) son en realidad software, que emulan a un terminal físico, pero sin hardware adicional. También se les llama software cliente, emuladores de terminal o simplemente terminales.

Aquí vamos a discutir como crear un emulador de terminal local, que se ejecute en el mismo sistema operativo donde correrá el proceso a controlar. Crear terminales remotos, como clientes telnet o SSH, es otra tarea.

¿UN EMULADOR DE QUE?

Un emulador de terminal es un programa que ejecuta un proceso hijo (por lo general una shell), y nos muestra gráficamente en una pantalla, lo que introducimos al proceso y las respuestas que este nos da. Es decir, un terminal tiene la característica, de poseer una parte visual (pantalla negra) que se muestra al usuario y que le permite interactuar con el proceso.

                        +---------------+            +-------------+
                        |               |            |             |
         ---escribe---> |   Emulador    | --stdin--> |             |
USUARIO                 |      de       |            |   Proceso   |
         <----leee----- |   Terminal    | <-stderr-- |     hijo    |
                        |               | <-stdout-- |             |
                        +---------------+            +-------------+

La idea de nuestro emulador de terminal, es poder controlar un proceso usando los flujos estándar de entrada salida (stdin y stdout), no una pantalla visible. Implementar toda el mecanismo de la pantalla de un terminal, implica conocer todas las secuencias de escape de un VT100, Vt180, y caracteres de control que no es un trabajo simple, y que no es el objetivo de este artículo.

¿Cuál es el objetivo de este artículo?

Lo que queremos aquí es crear un programa sencillo que haga las veces de emulador de terminal, pero que su salida la muestre, simplemente por el stdout y permita el ingreso de datos por el stidin. En pocas palabras, lo controlaremos por comandos (iba a poner «otro terminal», pero esto ya es demasiado comfuso).

El siguiente diagrama ayudará a entender nuestro objetivo:

                             +---------------+            +-------------+
                     stdin   |      Mi       |            |             |
  Ingreso de datos --------> |   Emulador    | --stdin--> |             |
                             |      de       |            |   Proceso   |
                     stdout  |   Terminal    | <-stderr-- |     hijo    |
  Recogo de datos <--------- |               | <-stdout-- |             |
                             +---------------+            +-------------+

En la práctica lo que lograremos (si es que lo logramos) será algo como esto:

USUARIO <–> Terminal Linux <–> Mi Emulador <–> Proceso Hijo

La combinación USUARIO-Teminal Linux, (equivalente a usuario sentado frente a su pantalla negra) se da por sentado, es por eso que no se especificó en el primer diagrama.

Pero ¿para qué poner un terminal dentro de otro terminal? ¿No sería mejor ahorrarnos este, aparentemente, inútil trabajo? ¿No es lo mismo quitar este paso intermedio? ¿Existe vida alienígena en Marte?

Bueno, si usted se hace estas preguntas, debo recordarle la motivación de este trabajo, que es poder automatizar procesos, y si bien los procesos pueden conversar muy bien por los flujos stdin, stdout y stderr, pues sucede que algunos programas, se niegan en enviar todo por estos canales y piden necesariamente un terminal. Ahí es donde entra en juego nuestro emulador de terminal. Lo que hace es como una especie de «geywey» (Gateway) que engañará al proceso para hacerle pensar que está trabajando en un terminal, mientras que para el otro lado, tendremos un simple stdin/stout de datos, que fácilmente podemos automatizar con algún programa.

Solo como información, todo este trabajo, es precisamente lo que hace el programa «expect», disponible para Linux. Si su problema es automatizar un proceso rebelde, (como el «sudo»), es posible que «expect» sea lo que usted necesite. Personalmente, debo decir que a la fecha no lo he probado, así que puede que esté hablando tonterías, pero lo pongo por, si resulta útil.

MI EMULADOR

Para cear un emulador de terminal, dentro de Linux, el primer paso es crear lo que se llama un pseudo terminal. Esto es en realidad es un par de archivos (dispositivos), que servirán como tubería para controlar al proceso hijo.  Luego se debe crear el proceso hijo, redireccionar sus flujos para el terminal creado (es como reconectar sus cables stdin, stdout y stderr) y luego, dejarlo ser.

Esto aparentemente, simple, no lo es en realidad. Pero las funciones de la GLIBC, incluyen funciones que nos ayudarán en esta tarea.

Para empezar, un pseudo terminal (PTY), requiere la creación de  dos dispositivos, el maestro (Master) y el Esclavo (Slave). Luego estos se ponen a trabajar juntitos en una suerte, de «dame que te doy». Ya que lo que ingresa como input al Mestro, sale como como Output en el esclavo y viceversa. El siguiente diagrama lo explica mejor:

                             Pseudo Terminal (PTY)
                          +-----------+-----------+            
                  input   |           |           |  output          
                --------> |           |           | ---------> 
                          |   Master  |  Slave    |            
                  output  |           |           |   input
               <--------- |           |           | <---------
                          +-----------+-----------+

El proceso a controlar, se enchufa, por el lado del esclavo, que está preparado para comportarse como un terminal físico, es decir, que soporta comandos típicos de una pantalla, como borrar pantalla, mover cursor, etc.

El lado del Master, es el que usamos para interactuar con el programa y a donde deberíamos conectar nuestra pantalla física, si es que la hubiera.

Para crear nuestro propio emulador de terminal en Linux, debemos acceder a funciones que pertenecen a la librería estándar de C: GLIBC. Pero eso no es problema, ya que desde Free Pascal tenemos acceso a muchas de estas funciones, y a las que no, podemos direccionarlas como referencias externas.

Las funciones requeridas son:

  • posix_openpt() -> Para crear el lado Master del PTY. Abre el dispositivo/dev/ptmx para obtener el descriptor de archivo del Master.
  • grantpt() -> Permite obtener privilegios sobre el lado del esclavo.
  • unlockpt() -> Desbloquea el lado del esclavo.
  • ptsname() -> Permite obtener el nombre del dispositivo esclavo.

Vamos a poner esto en la forma correcta, con el siguiente programa:

{Ejemplo de creación de un pseudo terminal en Linux. Este programa solo
 prepara el maestro y el esclavo de un PTY, pero no hace nada más.
 Por Tito Hinostroza.}
program project1;
uses process, BaseUnix;
const clib = 'c';

  function grantpt(__fd:cint):cint;cdecl;external clib name 'grantpt';
  function unlockpt(__fd:cint):cint;cdecl;external clib name 'unlockpt';
  function posix_openpt(__oflag:longint):longint;cdecl;external clib name 'posix_openpt';
  function ptsname(__fd:longint):Pchar;cdecl;external clib name 'ptsname';
var
  fdm, res: integer;
  s: string;
begin
  //Abre un pseudo terminal y devuelve un descriptor de archivo
  fdm := posix_openpt(O_RDWR);
  if fdm < 0 then begin
    writeln(stderr, 'Error ', errno, ' en posix_openpt()');
    ExitCode:=1;
    exit;
  end;
  //Obtiene privilegios sobre el esclavo de "fdm"
  res := grantpt(fdm);
  if res <> 0 then begin
    writeln(stderr, 'Error ', errno, ' en grantpt()');
    ExitCode:=1;
    exit;
  end;
  //Desbloquea el esclavo de "fdm"
  res := unlockpt(fdm);
  if res <> 0 then begin
    writeln(stderr, 'Error ', errno, ' en unlockpt()');
    ExitCode:=1;
    exit;
  end;
  //Lista /dev/pts
  if RunCommand('/bin/bash',['-c','ls -l /dev/pts'], s) then writeln(s);
  writeln('El dispositivo esclavo es: ', ptsname(fdm));
  ExitCode := 0;
end.

Este ejemplo sencillo, solo demuestra como se crea formalmente un PTY. Aparte de eso, resulta inútil. EL PTY no se usa, y se destruye al terminar el programa. El programa sin embargo, llega a mostrar la dirección física del esclavo del PTY.

Hay que notar que se hace uso de algunas funciones, directamente de las librerías de C, como rutinas externas.

Un ejemplo más útil sería si implementamos un proceso que haga algo simple, como responder lo mismo que se le envía.  El siguiente código  hace precisamente eso:

{Ejemplo de creación de un pseudo terminal en Linux. Este programa crea un
pseudo terminal, que solo responde lo mismo que le llega.
 Por Tito Hinostroza.}
program Project2;
uses termio, process, BaseUnix;
const
  clib = 'c';

  function grantpt(__fd:cint):cint;cdecl;external clib name 'grantpt';
  function unlockpt(__fd:cint):cint;cdecl;external clib name 'unlockpt';
  function posix_openpt(__oflag:longint):longint;cdecl;external clib name 'posix_openpt';
  function ptsname(__fd:longint):Pchar;cdecl;external clib name 'ptsname';

var
  fdm, fds, rc: integer;
  input: array [0..149] of char;
  slave_orig_term_settings: termios; // Saved terminal settings
  new_term_settings: termios; // Current terminal settings
begin
  //Abre un pseudo terminal y devuelve un descriptor de archivo
  fdm := posix_openpt(O_RDWR);
  if fdm < 0 then begin
    writeln(stderr, 'Error ', errno, ' en posix_openpt()');
    ExitCode:=1;
    exit;
  end;
  //Obtiene privilegios sobre el esclavo de "fdm"
  rc := grantpt(fdm);
  if rc <> 0 then begin
    writeln(stderr, 'Error ', errno, ' en grantpt()');
    ExitCode:=1;
    exit;
  end;
  //Desbloquea el esclavo de "fdm"
  rc := unlockpt(fdm);
  if rc <> 0 then begin
    writeln(stderr, 'Error ', errno, ' en unlockpt()');
    ExitCode:=1;
    exit;
  end;
  // Open the slave PTY
  fds := fpopen(ptsname(fdm), O_RDWR);
  writeln('The master side is named : ', ptsname(fdm));

  //Crea el proceso hijo
  if fpfork<>0 then begin
    //////////// Códido del proceso PADRE ///////////////
    {Este es el proceso con el que vamos a interactuar directamente}
    fpclose(fds);  //cierra el lado esclavo del PTY
    while true do begin   //lazo infinito
      //Operator's entry (standard input = terminal)
      write('Ingrese cadena para enviar: ');  //esto sale por el stdout
      rc := fpread(0, input, sizeof(input));  //espera hasta que haya datos
      if rc > 0 then begin
        //Envía cadena al proeso a través del PTY
        fpwrite(fdm, input, rc);
        //Extrae respuesta a través del PTY
        rc := fpread(fdm, input, sizeof(input) - 1);  //espera hasta que haya
        if rc > 0 then begin
          input[rc] := chr(0);  //pone marca de fin
          writeln(stdout, PChar(input));  //esto sale por el stdout
        end else begin
          break;  //sale del lazo "infinito"
        end;
      end else begin
        break;  //sale del lazo "infinito"
      end;
    end;
  end else begin
    //////////// Código del proceso HIJO ///////////////
    {Este es el proceso interactuará con el lado esclavo del PTY}
    fpclose(fdm);   //cierra el lado master del PTY
    //Guarda los parámetros del lado esclavo del PTY
    rc := tcgetattr(fds, slave_orig_term_settings);

    //Pone en modo "raw" en el lado esclavo del PTY
    new_term_settings := slave_orig_term_settings;
    cfmakeraw(new_term_settings);
    tcsetattr(fds, TCSANOW, new_term_settings);  //aplica cambios, ahora mismo

    //Desconecta los flujos del proceso hijo
    fpclose(0); // Cierra su stdin (current terminal)
    fpclose(1); // Cierra su stdout (current terminal)
    fpclose(2); // Cierra su stderr (current terminal)
    //Reconecta los flujos del proceso hijo
    fpdup(fds); // conecta al PTY: standard input (0)
    fpdup(fds); // conecta al PTY: standard output (1)
    fpdup(fds); // conecta al PTY: standard error (2)

    while true do begin  //lazo infinito
      rc := fpread(fds, input, sizeof(input) - 1);
      if rc > 0 then begin
        input[rc - 1] := chr(0);  //pone marca de fin
        //Escribe directamente en el stdout, que va a la entrada del esclavo
        writeln('El hijo responde: ', PChar(input));
      end else begin
        break;  //sale del lazo "infinito"
      end;
    end;
  end;
  ExitCode := 0;
end.

El trabajo de crear un proceso hijo, recae sobre la función fpfork(), que hace la magia de clonación del proceso actual.

EL PROGRAMA

Ahora vamos a dejar de lado los juegos y vamos a hacer algo más serio. Ahora controlaremos verdaderamente, un proceso hijo, creándole un PTY y capturaremos la salida/entrada, para redireccionarlo al stdin y stdout.

Esto logrará cumplir el objetivo inicial de este trabajo. Al programa lo he llamado titty y tiene el siguiente código:

{Titty
 Programa para Linux, que crea un terminal para controlar a un proceso, de modo
 que el proceso se comportará tal cual, como si se ejecutara en un terminal real.
 Este programa se comunica mediante los flujos comunes: stdin y stdout.
                                                Por Tito Hinostroza - Lima 2016.}
program titty;
uses termio, BaseUnix, strings, linux;
const
  clib = 'c';

  function grantpt(__fd:cint):cint;cdecl;external clib name 'grantpt';
  function unlockpt(__fd:cint):cint;cdecl;external clib name 'unlockpt';
  function posix_openpt(__oflag:longint):longint;cdecl;external clib name 'posix_openpt';
  function ptsname(__fd:longint):Pchar;cdecl;external clib name 'ptsname';
  function execvp(__file:Pchar; __argv:PPchar):longint;cdecl;external clib name 'execvp';

var
 fdm, fds: integer;
 rc , i: integer;
 input: array [0..149] of char;
 fd_in: TFDSet;
 slave_orig_term_settings: termios; // Saved terminal settings
 new_term_settings: termios; // Current terminal settings

 child_av: PPchar;  //necesario para execvp()
 nbytes: TsSize;

begin
  if argc <= 1 then begin
    writeln(stderr, 'Error de sintaxis.');
    writeln(stderr, '  Usar: ', argv[0], ' <programa_a_ejecutar>');
    exit;
  end;
  //Abre un pseudo terminal y devuelve un descriptor de archivo
  fdm := posix_openpt(O_RDWR);
  if fdm < 0 then begin
    writeln(stderr, 'Error ', errno, ' en posix_openpt()');
    ExitCode:=1;
    exit;
  end;
  //Obtiene privilegios sobre el esclavo de "fdm"
  rc := grantpt(fdm);
  if rc <> 0 then begin
    writeln(stderr, 'Error ', errno, ' en grantpt()');
    ExitCode:=1;
    exit;
  end;
  //Desbloquea el esclavo de "fdm"
  rc := unlockpt(fdm);
  if rc <> 0 then begin
    writeln(stderr, 'Error ', errno, ' en unlockpt()');
    ExitCode:=1;
    exit;
  end;
  // Abre el lado esclavo del PTY
  fds := fpopen(ptsname(fdm), O_RDWR);

  //Crea el proceso hijo
  if fpfork<>0 then begin   //hace la magia del "fork"
    //////////// Códido del proceso PADRE ///////////////
    {Este es el proceso con el que vamos a interactuar directamente}
    fpclose(fds);  //cierra el lado esclavo del PTY
    while true do begin   //lazo infinito
      // Espera datos por el master del PTY
      fpFD_ZERO(fd_in);
      fpFD_SET(0, fd_in);
      fpFD_SET(fdm, fd_in);
      //"fpselect" se detiene a esperar. Si no se desea esperar, se puede usar
      //fpselect(fdm + 1, @fd_in, nil, nil, 1), que da un desborde de 1 mseg.
      rc := fpselect(fdm + 1, @fd_in, nil, nil, nil);
      if rc = -1 then begin
        writeln(stderr, 'Error ', errno, ' en select()');
        ExitCode:=1;
        exit;
      end else begin
          // Verifica si hay algo en stdin de este programa
          if fpFD_ISSET(0, fd_in)<>0 then begin
            nbytes := fpread(0, input, sizeof(input));
            if (nbytes > 0) then begin
              //Lo envía al master del PTY, para que le llegue al proceso
              fpwrite(fdm, input, nbytes);
            end else begin
              if (nbytes < 0) then begin
                writeln(stderr, 'Error ', errno, ' en standard input');
                ExitCode:=1;
                exit;
              end;
            end;
          end;
          //Verifica si hay algo en el master del PTY
          if fpFD_ISSET(fdm, fd_in)<>0 then begin
            nbytes := fpread(fdm, input, sizeof(input));
            if (nbytes > 0) then begin
              //Lo envía al stdout, o dicho de otra forma, lo escribe en pantalla
              fpwrite(1, input, nbytes);  //podría ser un simple: writeln(input);
            end else begin
              if (nbytes < 0) then begin
                writeln(stderr, 'Error ', errno, ' en read master PTY');
                ExitCode:=1;
                exit;
              end;
            end;
          end;
      end;
    end;
  end else begin
    //////////// Código del proceso HIJO ///////////////
    {Este es el proceso interactuará con el lado esclavo del PTY}
    fpclose(fdm);   //cierra el lado master del PTY

    //Guarda los parámetros del lado esclavo del PTY
    rc := tcgetattr(fds, slave_orig_term_settings);
    //Pone en modo "raw" en el lado esclavo del PTY
    new_term_settings := slave_orig_term_settings;
    cfmakeraw(new_term_settings);
    tcsetattr(fds, TCSANOW, new_term_settings);  //aplica cambios, ahora mismo

    //Desconecta los flujos del proceso hijo
    fpclose(0); // Cierra su stdin (current terminal)
    fpclose(1); // Cierra su stdout (current terminal)
    fpclose(2); // Cierra su stderr (current terminal)
    //Reconecta los flujos del proceso hijo
    fpdup(fds); // conecta al PTY: standard input (0)
    fpdup(fds); // conecta al PTY: standard output (1)
    fpdup(fds); // conecta al PTY: standard error (2)

    fpclose(fds);   //ya no es útil este descriptor

    // Crea nueva sesión, y hace al proceso hijo, lider de la sesión.
    fpsetsid;

    // Fija el controlador del terminal al lado slave del PTY
    fpioctl(0, TIOCSCTTY, Pointer(1) );

    //Ejecuta el programa pasado como parámetro

    //Crea el arreglo de parámetros, para pasar a execvp()
    GetMem(child_av, argc*SizeOf(Pchar));
    for i := 1 to argc-1 do begin
      child_av[i - 1] := strnew(argv[i]);
    end;
    child_av[argc-1] := nil;   //marca de fin
    rc := execvp(child_av[0], child_av);

    // Si no hay error con el proceso, execvp(), nunca termina
    ExitCode := 1;
    exit;
  end;
  ExitCode := 0;
end.

El trabajo de este programa está en crear un proceso con el programa que se pasa como parámetro, y luego enchufarlo a un PTY, para poder interactuar con él, mediante el lado Master.

Las tareas de crear un proceso y lanzarlo recae en el código del proceso hijo. Aquí se hace uso de rutinas muy bajas, del sistema operativo, para crear el proceso requerido.

Con este programa podemos ahora controlar procesos rebeldes, que no quieren trabajar con stdin y stdout, sino que piden un terminal para operar. Si alguien todavía sigue preguntando: ¿Por qué complicarse tanto si puedo controlar a un proceso directamente usando stdin y stdout?, entonces no ha entendido el propósito de este artículo.

Aunque está demás, aclaro que este código es una aplicación de consola, para Linux. No funcionará en Windows. Además, como es una aplicación de consola, debe primero compilarse y usarse desde la línea de comando.

Una vez compilado, se puede llamar desde línea de comandos en la siguiente forma:

$ ./titty ls -l

Lo que se obtiene en pantalla, será un listado de los archivos. Este listado se ha obtenido leyendo el master del PTY, del proceso «ls -l»:

Sin título

Al final del listado, aparecerá un mensaje de error, al que debemos interpretar, simplemente, como un aviso de que la aplicación ha terminado.

De la misma forma, se puede interactuar con diversos programas que manejen el terminal.

Hay mucho que comentar sobre este programa pero ya es demasiado por hoy.


1 comentario

Dejar una contestacion

Tu dirección de correo electrónico no será publicada.


*