Redirigir el resultado de un proceso en Visual Basic .NET

Fecha: 10/Feb/2005 (8 de Febrero del 2005)
Autor: José Miguel Torres ([email protected])

 


Con la integración de aplicaciones de consola o con la ejecución de comandos desde el interprete de comandos son muchas las veces que se nos ha ocurrido la manera de que el resultado de éstas se redirijan a una cadena para que nosotros luego hagamos uso de ello. Pues aquí tienen un ejemplo en .NET de una evolución de Visual Basic 6.0.

Contenido

Redirigir la entrada/salida de procesos.
Utilización y necesidad.
De Visual Basic 6.0 a VB .NET
La clase System.Diagnostics.Process.
Conclusión

Redirigir la entrada/salida de procesos.

Un buen día se me ocurrió, a través de una necesidad de un departamento de sistemas, desarrollar una aplicación en Visual Basic 6.0, aún .NET no existía, que ejecutara comandos de red, de tipo ping, netstat, nbtstat, etcétera y que el resultado lo redirigiera a un Text Box de la aplicación para que a partir de aquí pudiera ser impreso o guardado en un archivo con la facilidad de tener todos los comandos en un solo clic del mismo formulario.

Figura 1. Aplicación desarrollada en Visual Basic 6.0 para la ejecución de comandos en red.

Lo conseguí (Figura 1), pero no fue fácil. Tuve que investigar mucho e ir probando un trozo de código por aquí, una función de un código open source por allí, hasta que creé un método que a través de unas funciones externas ejecutaba un proceso nuevo y redirigía, a través de ficheros, el resultado a un Text Box. No es el objetivo de éstas líneas explicar el desarrollo en Visual Basic 6.0, pero como ‘pincelada’ les diré que, entre otras, utilicé APIs como CreatePipe, ReadFile, CreateProcessA o WaitForSingleObject.

El problema fundamental, una vez desarrollado, es que en ocasiones, con ejecución de comandos ‘pesados’ o de manera eventual el proceso que generaba para la ejecución del comando se colgaba, y la aplicación Visual Basic 6.0 esperaba a que ésta finalizara así que entraba en un ‘coma profundo’ en la que la mayoría de veces había que finalizar el proceso principal. Uno de los planteamientos en .NET es que esto no sucediera, es decir, que fueran las propias excepciones las que controlasen esto, ya que el proceso se iniciaba desde el mismo CLR.

  Cuando decidí pasar ese código a .NET, en un primer momento, leí acerca del espacio de nombres System.Diagnostics y System.Threads y entendí que éstos facilitarían el trabajo de migración, lo que no me imaginé en ningún momento es hasta que punto.

De Visual Basic 6.0 a .NET.

En primer lugar me gustaría que le echaran un vistazo al resultado del método que ejecutaba un comando y retornaba el resultado en un String en Visual Basic 6.0:

 

Private Function ExecCmdPipe(ByVal CmdLine As String) As String
   
'Executes the command, and when it finish returns value to VB

 

    Dim proc As PROCESS_INFORMATION, ret As Long, bSuccess As Long
   
Dim start As STARTUPINFO
   
Dim sa As SECURITY_ATTRIBUTES
   
Dim hReadPipe As Long, hWritePipe As Long
   
Dim bytesread As Long, mybuff As String
   
Dim i As Integer

    Dim sReturnStr As String

    ' the lenght of the string must be 10 * 1024

    mybuff = String(10 * 1024, Chr$(65))
   
sa.nLength = Len(sa)
   
sa.bInheritHandle = 1&
   
sa.lpSecurityDescriptor = 0&
   
ret = CreatePipe(hReadPipe, hWritePipe, sa, 0)
   
If ret = 0 Then
       
'===Error
       
ExecCmdPipe = "Error: CreatePipe failed. " & Err.LastDllError
       
Exit Function
   
End If

    start.cb = Len(start)
   
start.hStdOutput = hWritePipe
   
start.dwFlags = STARTF_USESTDHANDLES + STARTF_USESHOWWINDOW
   
start.wShowWindow = SW_SHOWMINNOACTIVE
 

    ' Start the shelled application:
   
ret& = CreateProcessA(0&, CmdLine$, sa, sa, 1&, _
       
NORMAL_PRIORITY_CLASS, 0&, 0&, start, proc)
   
If ret <> 1 Then
       
'Error
       
sReturnStr = "Error: CreateProcess failed. " & Err.LastDllError
   
End If

    ' Wait for the shelled application to finish:
   
ret = WaitForSingleObject(proc.hProcess, INFINITE)
    bSuccess = ReadFile(hReadPipe, mybuff, Len(mybuff), bytesread, 0&)
   
If bSuccess = 1 Then
       
sReturnStr = Left(mybuff, bytesread)
   
Else
       
'===Error
       
sReturnStr = "Error: ReadFile failed. " & Err.LastDllError
   
End If

    ret = CloseHandle(proc.hProcess)
   
ret = CloseHandle(proc.hThread)
   
ret = CloseHandle(hReadPipe)
   
ret = CloseHandle(hWritePipe)
 
   
'returns to VB
   
ExecCmdPipe = sReturnStr

End
Function

 

¡No se asusten! Les he adjuntado el proyecto completo junto a estas líneas, si lo abren y lo estudian, verán que no es tan descabellado. A simple vista hay varias llamadas a API’s con parámetros de estructuras de tipos, y constantes hexadecimales que permiten la operación, que no aparecen ya que sólo les he indicado el método.

Cuando me plantee la migración supuse que necesitaría realizar llamadas a funciones externas al Kernel32 y que debería ‘batallar’ con el cálculo de referencias de las estructuras y demás. Que éste código debería ser llamado por los métodos de la clase Process y que la integración no sería fácil y segura así que la utilización de delegados para los eventos que pudieran suceder y la creación de excepciones de usuario serían necesarias.....

Pues estaba equivocado, échenle un vistazo al código en VB .NET:

Private Sub Ejecutar(ByVal comando As String, ByVal argumento As String)
   
Me.Cursor = Cursors.WaitCursor
   
Me.sbP1.Text = "Ejecutando ... "
   
Me.sbP2.Text = comando + " " + argumento
   
Me.lbInfo.Items.Clear()
   
Me.txtOut.Clear()
   
Me.Refresh()
   
Dim cmd As System.Diagnostics.Process = New System.Diagnostics.Process
   
cmd.EnableRaisingEvents = True
   
cmd.Exited += New EventHandler(cmd_Exited)
   
cmd.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
   
cmd.StartInfo.CreateNoWindow = True
   
cmd.StartInfo.RedirectStandardOutput = True
   
cmd.StartInfo.RedirectStandardInput = False
   
cmd.StartInfo.UseShellExecute = False
   
cmd.StartInfo.FileName = comando
   
cmd.StartInfo.Arguments = argumento
   
Try
       
cmd.Start()
       
Me.lbInfo.Items.Add("Ejecutado " + comando)
       
Dim r As String = cmd.StandardOutput.ReadToEnd
       
cmd.WaitForExit(1000 * 5)
       
Me.txtOut.Text = r
       
Me.lbInfo.Items.Add("Id del proceso " + cmd.Id.ToString)
       
Me.lbInfo.Items.Add("Ejecutado en " + cmd.MachineName)
       
Me.lbInfo.Items.Add("Finalizado a las " + cmd.ExitTime.ToLongTimeString)
   
Catch ex As System.Exception
       
Me.txtOut.Text = ex.Message
   
End Try
   
Me.Cursor = Cursors.Default

End
Sub

Sí; lo que ven equivale e incluso supera al código de Visual Basic 6.0. Este método ejecuta un comando con sus parámetros y retorna el resultado como string. A partir de ahí decidí crear un proyecto para Windows colocando varios buttons, un textBox de resultados y un listBox de información. El resultado lo ven en la figura 2.

     

Figura 2. Interfície en .NET para la ejecución de comandos de red.

Los comando que introduje son de izquierda a derecha según la ilustración netstat –a, netstat –r, ipconfig, netstat –e (cuyo resultado sale en la ilustración de la figura 2), nbtstat –s. Esto es sólo un ejemplo de lo que se puede hacer.

Veamos ahora como se desarrolla en .NET y que posibilidades tiene.

La clase System.Diagnostics.Process.

La llamada o ejecución a una aplicación (nuevo proceso) es relativamente sencilla en .NET. No es necesario la llamada a las tediosas (aunque a veces necesarias) funciones externas, para ello, con la clase Process podemos realizar muchas operaciones que hasta entonces eran muy difíciles.

Para la creación en primer lugar instanciamos un objeto de la clase Process. Posteriormente debemos configurar dicho objeto para que cuando ejecute el nuevo proceso tenga el comportamiento que deseamos. En el ejemplo que les he detallado, indicamos que el proceso haga uso de un evento de finalización llamado Exited y a continuación se le indica el método que se ejecutará cuando se ejecute dicho evento:

    Dim cmd As System.Diagnostics.Process = New System.Diagnostics.Process
   
cmd.EnableRaisingEvents = True
   
cmd.Exited += New EventHandler(cmd_Exited)

En la aplicación, la ejecución de cualquier comando nos gustaría que se hiciera de modo silencioso, con lo cual le indicaremos que la ventana que genere, si la genera, sea oculta. Digo si la genera, porque también le podemos indicar que no la genere, con lo cual el modo silencioso sea aún más creíble.

    cmd.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
   
cmd.StartInfo.CreateNoWindow = True

A continuación configuraremos y redirigimos la entrada, salida y error. Por defecto dejaremos la propiedad de entrada RedirectStandardInput intacta, con el valor False. Al de salida RedirectStandardOutput le asignamos un valor True para indicarle que ésta, o sea el resultado del proceso, no será el que tiene asignado por defecto. Le indicamos también que no utilice el shell del sistema operativo para la ejecución del proceso mediante la propiedad UseShellExecute = False. Por último, cabe la posibilidad de redirigir la salida en caso de error mediante la propiedad RedirectStandardError. En  el ejemplo no hacemos uso de ella. 

    cmd.StartInfo.RedirectStandardOutput = True
   
cmd.StartInfo.RedirectStandardInput = False
   
cmd.StartInfo.UseShellExecute = False

Ahora es tiempo de indicar que comando debe ejecutar y con que argumentos. Para ello utilizaremos FileName y Arguments que será la información necesaria para que cuando iniciemos la llamada mediante Start se ejecute el proceso de manera satisfactoria. El resultado de la ejecución del proceso lo obtenemos de cmd.StandardOutput.ReadToEnd(). Y le indicaremos al objeto que se espere hasta que finalice el proceso mediante el método WaitForExit(5000) al cual podemos indicarle un valor en milisegundos máximo, 5 segundos en el ejemplo.

     cmd.Start()
    
Dim r As String = cmd.StandardOutput.ReadToEnd
    
cmd.WaitForExit(1000 * 5)

Las posibilidades a partir de aquí son variadas. Con las propiedades ExitTime, Id, Machine, pueden obtener la hora de finalización, el identificador de proceso o la máquina de ejecución de manera directa, por ejemplo. En el método que hemos asignado el evento Exited indicaremos mediante la interfaz gráfica la información de éste. 

Conclusión

Con este ejemplo pueden empezar a desarrollar aplicaciones cuyo resultado se integre en la interfície de su programa. La posibilidades son variadas pero mucho más fáciles que años atrás.

Les invito a resolver cualquier duda o intercambiar cualquier observación al respecto.


Espacios de nombres usados en el código de este artículo:

System.Diagnostics
System.Threads 


 

ir al índice

Fichero con el código de ejemplo (para VB6): jtorres_redirigirVB_Code_NetStat_VB6.zip - 21.3 KB