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»:
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.
¿Cómo citar este artículo?
- En APA: Hinostroza, T. (4 de diciembre de 2016). El inicio de un Terminal, con Linux y Free Pascal. Blog de Tito. https://blogdetito.com/2016/12/04/el-inicio-de-un-terminal-con-linux-y-free-pascal/
- En IEEE: T. Hinostroza. (2016, diciembre 4). El inicio de un Terminal, con Linux y Free Pascal. Blog de Tito. [Online]. Available: https://blogdetito.com/2016/12/04/el-inicio-de-un-terminal-con-linux-y-free-pascal/
- En ICONTEC: HINOSTROZA, Tito. El inicio de un Terminal, con Linux y Free Pascal [blog]. Blog de Tito. Lima Perú. 4 de diciembre de 2016. Disponible en: https://blogdetito.com/2016/12/04/el-inicio-de-un-terminal-con-linux-y-free-pascal/
Excelente