Tutorial 21: Tubería

En este tutorial, exploraremos la tubería [pipe], qué es y qué podemos hacer por él. Para hacerlo más interesante, me meto en la técnica de cómo cambiar el color del fondo y del texto de una ventana de edición.

Bajar el ejempo aquí.

Teoría:

Una tubería [pipe] es un conducto o vía de comunicación entre dos terminales. Puedes usar una tubería para intercambiar datos entre dos procesos diferentes, o dentro del mismo proceso. Es como un walkie-talkie. Das a la otra parte una configuración y esta parte puede usarla para comunicarse contigo.

Hay dos tipos de tuberías: anónima y con nombre. Una tubería anónima es, como lo dice el nombre, anónima: es decir, puedes usarla sin saber su nombre. Una tubería nombrada es lo opuesto: tienes que conocer su nombre antes de usarla.

También puedes clasificar las tuberías de acuerdo a su propiedad: un-sentido (one-way) o dos-sentidos (two way). En una tubería de un sentido, los datos pueden fluir sólo en una dirección: de un terminal a otro. Mientras que en una tubería de dos sentidos, los datos pueden ser intercambiados entre dos terminales.

Una tubería anónima siempre es de un sentido mientras que una tubería nombrada puede ser de un sentido o de dos sentidos. Usualmente se usa una tubería nombrada en un entorno de red donde un servidor puede conectarse a varios clientes.

En este tutorial, examinaremos con cierto detalle las tuberías anónimas. El propósito principal de una tubería anónima es ser usada como un canal de comunicación entre un proceso padre y un proceso hijo o entre procesos hijos.

La tubería anónima es realmente útil cuando tratamos con una aplicación de cónsola. Una aplicación de cónsola es un tipo de programa win32 que usa una cónsola para su entrada y su salida. Una consola es como una caja DOS. Sin embargo, una aplicación de cónsola es un programa totalmente en 32-bit. Puede usar cualquier función GUI, como otros porgramas GUI. Lo que ocurre es que tiene una cónsola para su uso.

Una aplicación de cónsola tiene tres manejadores [handles] que pueden usarse para entrada y salida. Los llamamos manejadores estándard. Hay tres de ellos: entrada estándard, salida estándar y error estándard. El manejador de entrada estándard es usado para leer/recobrar la información de la cónsola y el manejador de salida eestándar es usado para información salida/impresión para la cónsola. El manejador de error estándar es usado para reportar condiciones de error ya que su salida no puede ser redireccionada.

Una aplicación de cónsola puede recuperar estos tres manejadores llamando a la función GetStdHandle, especificando el manejador que quiere obtener. Una aplicación GUI no tiene una cónsola. Si llamas a GetStdHandle, regresará un error. Si en realidad quieres usar una consola, puedes llamar a AllocConsole para localizar una nueva cónsola. Sin embargo, no olvides llamar a FreeConsole cuando hayas hecho lo que tienes que hacer con la cónsola.

Con más frecuencia se emplea una tubería anónima para redireccionar entrada y/o salida de una aplicación de cónsola hija. Para que esto trabaje el proceso padre puede ser una aplicación de cónsola o una GUI, pero el proceso hijo debe ser una aplicación de cónsola. Como debes saber, una aplicación de cónsola usa manejadores estándard para su entrada y salida. Si queremos redireccionar entrada y/o salida de una aplicación de cónsola, podemos reemplazar su manejador con el manejador de un terminal de una tubería. Una aplicación de cónsola no sabe que está usando el manejador de un terminal de una tubería. Lo usará como un manejador estándar. Esto es un tipo de polimorfismo, como se diría en la jerga POO [OOP: Object Oriented Programming = Programación Orientada a Objetos]. Esta aproximación al problema es poderosa ya que no necesitamos modificarde ninguna manera el proceso hijo .

Otra cosa que deberías saber sobre las aplicaciones de cónsola es de donde obtiene los manejadores estándar. Cuando se crea una aplicación de cónsola, el proceso padre tiene dos posiblidades: puede crear una nueva cónsola para la hija o puede dejar que la hija herede su propia cónsola. Para que trabaje la segunda opción, el proceso padre debe ser una aplicación de cónsola, pero si es una aplicación GUI, debe llamar primero a AllocConsole para localizar una cónsola.

Comencemos a trabajar. Con el fin de crear una tubería anónima necesitas llamar a CreatePipe. CreatePipe tiene el siguiente prototipo:

CreatePipe proto pReadHandle:DWORD, \
       pWriteHandle:DWORD,\
       pPipeAttributes:DWORD,\
       nBufferSize:DWORD

Si la llamada tiene éxito, el valor regresado es distinto de cero. Si falla, el valor regresado es cero.

Después de que la llamada tiene éxito, obtendrás dos manejadores, uno para el terminal de lectura de la tubería y otro para el terminal de escritura.

Ahora resumiremos los pasos para redireccionar la salida estándard de un programa de cónsola hijo hacia nuestro propio proceso. Nota que mi método difiere del de la referencia de la api de win32 suministrada por Borland. El método en la referencia de la api win32 asume que el proceso padre es una aplicación de cónsola y por eso el proceso hijo puede heredar los manejadores estándar de él. Pero en muchas ocasiones, necesitaremos redireccionar la salida desde una aplicación de cónsola a una GUI.

  1. Crear una tubería anónima con CreatePipe. No olvides establecer el miembro bInheritable de SECURITY_ATTRIBUTES a TRUE para que el manejador sea heredable.
  2. Ahora debemos preparar los parámetros que pasaremos a CreateProcess ya que los usaremos para cargar la aplicación de cónsola hija. Otra estructura importante es STARTUPINFO. Esta estructura determina la apariencia de la ventana principal del proceso hijo cuando aparece por vez primera. Esta estructura es vital para nuestro propósito. Puedes esconder la ventana principal y pasar el manejador de la tubería al proceso de cónsola hijo con ella. Abajo están los miembros que debes llenar:
  3. Llamar a CreateProcess para cargar la aplicación hija. Después de que CreateProcess ha tenido éxito, la hija todavía está durmiendo. Es cargada en la memoria pero no corre de inmediato. Cerrar el manejador de escritura de la tubería. Esto es necesario. Porque el proceso padre no tiene uso para el manejador de escritura de la tubería, y la pipa no trabajará si hay más de un terminal de escritura, DEBEMOS cerrarlos antes de leer los datos de la tubería. Sin embargo, no cierres el manejador de escritura antes de llamar a CreateProcess, porque sino tu pipa se romperá. Deberías cerrarla justo después del regreso de CreateProcess y antes de leer los datos del terminal de lectura de la tubería.
  4. Ahora puedes leer los datos del terminal de lectura de la piap con ReadFile. Con ReadFile, you kick dentro del proceso hijo en modo de ejecución [running mode]. Iniciará la ejecución y cuando escriba algo al manejador de salida estándar (el cual es realmente el manejador al termianl de escritura de la tubería), los datos serán enviados a través de la tubería al terminal de lectura. Puedes pensar en ReadFile como una extracción de datos del terminal de lectura de la tubería. Debes llamar a ReadFile repetidas veces hasta que retorne 0 lo cual significa que ya no hay más datos que leer. Puedes hacer algo con los datos que lees de la tubería. En nuestro ejemplo, los pongo dentro del control de edición.
  5. Cerrar el manejador de lectura de la tuberia.

Ejemplos:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU equ 101         ; el ID del menú principal
IDM_ASSEMBLE equ 40001

.data
ClassName            db "PipeWinClass",0
AppName              db "One-way Pipe Example",0 EditClass db "EDIT",0
CreatePipeError     db "Error during pipe creation",0
CreateProcessError     db "Error during process creation",0
CommandLine     db "ml /c /coff /Cp test.asm",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?

.code
start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:HWND
    mov wc.cbSize,SIZEOF WNDCLASSEX
    mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc
    mov wc.cbClsExtra,NULL
    mov wc.cbWndExtra,NULL
    push hInst
    pop wc.hInstance
    mov wc.hbrBackground,COLOR_APPWORKSPACE
    mov wc.lpszMenuName,IDR_MAINMENU
    mov wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov wc.hIcon,eax
    mov wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov wc.hCursor,eax
    invoke Reg es terClassEx, addr wc
    invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\ CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL
    mov hwnd,eax
    .while TRUE
        invoke GetMessage, ADDR msg,NULL,0,0
        .BREAK .IF (!eax)
        invoke TranslateMessage, ADDR msg
        invoke D es patchMessage, ADDR msg
    .endw
    mov eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    LOCAL rect:RECT
    LOCAL hRead:DWORD
    LOCAL hWrite:DWORD
    LOCAL startupinfo:STARTUPINFO
    LOCAL pinfo:PROCESS_INFORMATION
    LOCAL buffer[1024]:byte
    LOCAL bytesRead:DWORD
    LOCAL hdc:DWORD
    LOCAL sat:SECURITY_ATTRIBUTES
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+ WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0, hWnd, NULL, hInstance, NULL
        mov hwndEdit,eax
    .elseif uMsg==WM_CTLCOLOREDIT
        invoke SetTextColor,wParam,Yellow
        invoke SetBkColor,wParam,Black
       invoke GetStockObject,BLACK_BRUSH
        ret
    .elseif uMsg==WM_SIZE
        mov edx,lParam
        mov ecx,edx
        shr ecx,16
        and edx,0ffffh
        invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE
    .elseif uMsg==WM_COMMAND
       .if lParam==0
            mov eax,wParam
            .if ax==IDM_ASSEMBLE
                mov sat.niLength,sizeof SECURITY_ATTRIBUTES
                mov sat.lpSecurityDescriptor,NULL
                mov sat.bInheritHandle,TRUE
                invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
                .if eax==NULL
                    invoke MessageBox, hWnd, addr CreatePipeError, addr AppName, MB_ICONERROR+ MB_OK
                .else
                    mov startupinfo.cb,sizeof STARTUPINFO
                    invoke GetStartupInfo,addr startupinfo
                    mov eax, hWrite
                    mov startupinfo.hStdOutput,eax
                    mov startupinfo.hStdError,eax
                    or startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
                    mov startupinfo.wShowWindow,SW_HIDE
                    invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo
                    .if eax==NULL
                        invoke MessageBox,hWnd,addr CreateProcessError,addr         AppName,MB_ICONERROR+MB_OK
                    .else
                        invoke CloseHandle,hWrite
                        .while TRUE
                            invoke RtlZeroMemory,addr buffer,1024
                            invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
                            .if eax==NULL
                                .break
                            .endif
                            invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
                            invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
                        .endw
                    .endif
                    invoke CloseHandle,hRead
                .endif
            .endif
        .endif
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

Análisis:

El ejemplo llamará a ml.exe para ensamblar un archivo llamado test.asm y redireccionar la salida de ml.exe al control de edición en su área cliente.

Cuando el programa es cargado, registra la clase de ventana y crea la ventana principal como es usual. Lo primero que hace durante la creación de la ventana principal es crear un control de edición que será usado paera desplegar la salida de ml.exe.

Ahora la parte interesante, cambiaremos el color del texto y del fondo del control de edición. Cuando un control de edición va a pintar su área cliente, envía el mensaje WM_CTLCOLOREDIT a su padre.

wParam contiene el manejador del dispositivo de contexto que usará el control de edición para escribir su propia área cliente. Podemos aprovechar esto para cambiar las características de HDC.

    .elseif uMsg==WM_CTLCOLOREDIT
        invoke SetTextColor,wParam,Yellow
        invoke SetTextColor,wParam,Black
        invoke GetStockObject,BLACK_BRUSH
        ret

SetTextColor cambia el color del texto a amarillo. SetTextColor cambia el color de fondo del texto a negro.Y finalmente, obtenemos el manejador a la brocha negra que queremos regresar a Windows. Con el mensaje WM_CTLCOLOREDIT, debes regresar un manejador a la brocha que Windows usará para pintar el fondo del control de edición. En nuestro ejemplo, quiero un fondo negro así que regreso a Windows un manejador a la brocha negra.

Ahora el usuario selecciona el elemento de menú Assemble, crea una tubería anónima.

            .if ax==IDM_ASSEMBLE
                mov sat.niLength,sizeof SECURITY_ATTRIBUTES
                mov sat.lpSecurityDescriptor,NULL
                mov sat.bInheritHandle,TRUE

Antes de llamar a CreatePipe, primero debemos llenar la estrcutura SECURITY_ATTRIBUTES. Nota que podemos usar NULL en el miembro lpSecurityDescriptor si nos tiene sin cuidado la seguridad. Y el miembro bInheritHandle debe ser TRUE para que los manejadores de la tubería sean heredados al proceso hijo.

               invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL

 Después de eso llamamos a CreatePipe que, si tiene éxito, llenará las variables hRead y hWrite con los manejadores a los terminales de lectura y escritura respectivamente.

                    mov startupinfo.cb,sizeof STARTUPINFO
                    invoke GetStartupInfo,addr startupinfo
                    mov eax, hWrite
                    mov startupinfo.hStdOutput,eax
                    mov startupinfo.hStdError,eax
                    mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
                    mov startupinfo.wShowWindow,SW_HIDE

Ahora vamos a llenar la estructura STARTUPINFO. Llamamos a GetStartupInfo para llenar la estructura STARTUPINFO con los valores por defecto del proceso padre. Debes llenar la estructura STARTUPINFO con esta llamada si quieres que tu código trabaja con win9x y NT. Después que regrese la llamada a GetStartupInfo, puedes modificar los miembros que son importantes. Copiamos el manejador del terminal de escritura de la tubería dentro de hStdOutput y hStdError ya que queremos que el proceso hijo lo use en vez de los manejadores estándar de salida/error. También queremos esconder la ventana de la cónsola del proceso hijo, así que ponemos el valor SW_HIDE dentro del miembro wShowWidow. Por último, debemos indicar que los miembros hStdOutput, hStdError y wShowWindow son válidos y deben ser usados especificando las banderas STARTF_USESHOWWINDOW y STARTF_USESTDHANDLES en el miembro dwFlags.

                   invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo

Ahora creamos el proceso hijo con la llamada a CreateProcess. Nota que el parámetro bInheritHandles debe ser establecido a TRUE para que trabaje el manejador de la tubería.

                       invoke CloseHandle,hWrite

Después de que creamos satisfactoriamente el proceso hijo, debemos cerrar el terminal de escritura de la tubería. Recuerda que pasamos el manejador de escritura al proceso hijo a través de la estructura STARTUPINFO. Si no cerramos el manejador de escritura de nuestro terminal, habrán dos terminales de escritura. Y la tubería no trabajará. Debemos cerrar el manejador de escritura después de CreateProcess pero antes de leer los datos del terminal de lectura.

                        .while TRUE
                            invoke RtlZeroMemory,addr buffer,1024
                            invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
                            .if eax==NULL
                                .break
                            .endif
                            invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
                            invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
                        .endw

Ahora estamos listos para leer los datos de la salida estándar del proceso hijo. Nos mantenemos en un bucle infinito hasta que no hayan más datos que leer desde el terminal de lectura de la tubería. Llamamos a RtlZeroMemory para llenar el buffer con ceros y luego llmamos a ReadFile, pasando el manejador de lectura de la tubería en lugar de un manejador de archivo. Nota que sólo leemos un máximo de 1023 bytes ya que necesitamos que los datos sean una cadena ASCIIZ que podemos pasar al control de edición

Cuando regresa ReadFile con los datos en el buffer, llenamos los datos dentro del control de edición. Sin embargo, aquí hay un pequeño problema. Si usamos SetWindowText para poner los datos dentro del control de edición, los nuevos datos sobreescribirán los datos existentes! Queremos que los datos se anexen al final de los datos existentes.

Para alcanzar esa meta, primero movemos la careta alfinal del texto en el control de edición enviando el mensaje EM_SETSEL con wParam==-1. Luego, anexamos los datos en ese punto enviando el mensaje EM_REPLACESEL.

                   invoke CloseHandle,hRead

Cuando ReadFile retgresa NULL, rompemos el bucle y cerramos el manejador de escritura.


Index

Next

Iczelion's Win32 Assembly Homepage

n u M I T_o r's   Programming Page

Este tutorial, original de Iczelion, ha sido traducido por:   n u M I T_o r

www.000webhost.com