Redirigir el resultado de un proceso en Visual Basic .NET Fecha: 10/Feb/2005 (8 de Febrero del 2005)
|
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ónRedirigir 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 Ifstart.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 Ifret = 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 SubSí; 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
Fichero con el código de ejemplo (para VB6): jtorres_redirigirVB_Code_NetStat_VB6.zip - 21.3 KB