Ese código es muy antiguo y es una guarrada (todo hay que decirlo), yo no lo escribí:
http://stackoverflow.com/a/15906638/1248295El algoritmo de ese código se basa en la comparación de ciertos parámetros de consumo del proceso para determinar si el proceso está "inactivo". Cada 1500 milisegundos, mediante un Timer, comprueba si ha habido un incremento en el tamaño de alojamiento de memoria privada del proceso, y el tiempo de ejecución en modo kernel.
Cuando inicias un proceso se disparan muchos parámetros, entre ellos los que ya he mencionado, y cuando la interfáz de usuario del proceso se ha cargado completamente, los valores de esos parámetros dejan de incrementar (o al menos deberían, los bugs y/o fugas de memoria serían una excepción, jeje), y en ese preciso momento podemos concluir (aunque no en todos los casos) que la interfaz de usuario se ha cargado por completo, por que no está gastando más recursos.
No existe una ciencia ni botón mágico con el que poder determinar cuando la interfaz de usuario de un proceso externo se ha cargado completamente, de hecho hay interfaces de usuario llenas de animaciones que siempre están generando recursos (como por ejemplo el cliente de
Battle.net de
Blizzard) y eso es un problema muy grande para hacer nuestros cálculos.
En .NET podemos hacer uso del método
Process.WaitforInputIdle() pero esto es una metología engañosa, puesto que solo sirve para esperar a que la cola de mensajes de la aplicación entre en un estado Idle, es decir, cuando esté lista para recibir mensajes de entrada, el problema es que muchas aplicaciones (como or ejemplo Steam) entran en ese estado mientras está cargando la UI ...mucho antes, durante la etapa de
startup.
Bueno, despues de todo este rollo que he soltado para explicar varios aspectos, vamos a lo que te interesa, el código:
En el código fuente de mi API gratuita
ElektroKit puedes encontrar una refactorización que le hice a esa guarrada de código que publiqué en su día, estoy seguro que te será mucho más sencilla de entender:
Public Shared Sub WaitUntilLoaded(ByVal p As Process,
Optional ByVal timeOut As Integer = 1500)
Dim cpuChanged As Boolean = True
Dim memChanged As Boolean = True
Dim oldCpu As Double, newCpu As Double
Dim oldMem As Long, newMem As Long
While ((cpuChanged OrElse memChanged) = True)
Do Until (p.TotalProcessorTime.TotalMilliseconds <> 0.0R)
Thread.Sleep(10)
Loop
If (p Is Nothing) OrElse (p.HasExited) Then
Exit While
Else
newMem = p.PrivateMemorySize64
memChanged = (newMem <> oldMem)
oldMem = newMem
newCpu = p.TotalProcessorTime.TotalMilliseconds
cpuChanged = (newCpu <> oldCpu)
oldCpu = newCpu
Thread.Sleep(timeOut)
End If
End While
End Sub
De hecho, cualquier sippet mio que encuentres en el foro probablemente con el paso del tiempo lo haya mejorado e integrado en
ElektroKit, solo tienes que buscar por el nombre de la función, o preguntarme.
Hoy me he tomado un rato para volver a refactorizar el código, no me convencia demasiado la manera de usarlo, así que he decidido convertirlo en extensiones de método, y he implementado una lógica basada en WMI, ¿por qué?, por que aparte de servirnos para analizar mayor cantidad de parámetros como por ejemplo la cantidad de operaciones de transferencia de lectura/escritura del proceso, además nos sirve para evitar conflictos al intentar leer los parámetros de un exe de 64-bits desde un exe de 32 Bits ...en caso de que lo necesitemos.
En fin, aquí tienes el código (le he eliminado la documentación Xml no-esencial para que no ocupe demasiado espacio):
Public Module ProcessExtensions
''' ----------------------------------------------------------------------------------------------------
''' <summary>
''' Causes the <see cref="Process"/> component to wait indefinitely
''' until the user interface has been fully loaded then entered in IDLE state.
''' <para></para>
''' This applies only to processes with a user interface and, therefore, a message loop.
''' </summary>
''' ----------------------------------------------------------------------------------------------------
<DebuggerStepThrough>
<Extension>
Public Sub WaitForIdle(ByVal sender As Process)
ProcessExtensions.WaitForIdle(sender, checkInterval:=1500)
End Sub
''' ----------------------------------------------------------------------------------------------------
''' <summary>
''' Causes the <see cref="Process"/> component to wait for the specified amount of time, in milliseconds,
''' until the user interface has been fully loaded then entered in IDLE state.
''' <para></para>
''' This applies only to processes with a user interface and, therefore, a message loop.
''' </summary>
''' ----------------------------------------------------------------------------------------------------
''' <param name="checkInterval">
''' The interval, in milliseconds, to check the parameters that determines
''' whether the user-interface has been loaded and the preocess entered in IDLE state.
''' <para></para>
''' It is recommended to experiment with a value between <c>1000</c> and <c>2000</c> ms,
''' smaller values could give unexpected results.
''' </param>
''' ----------------------------------------------------------------------------------------------------
<DebuggerStepThrough>
<Extension>
Public Sub WaitForIdle(ByVal sender As Process, ByVal checkInterval As Integer)
If (checkInterval < 0) Then
Throw New ArgumentOutOfRangeException(paramName:="checkInterval")
Exit Sub
End If
' Total time of the process running in kernel mode, in milliseconds.
' This is the same as Process.TotalProcessorTime
Dim oldCpuTime As ULong, newCpuTime As ULong
Dim cpuTimeChanged As Boolean = True
' Current number of pages allocated that are only accessible to the process.
' This is the same as Process.PrivateMemorySize64
Dim oldMemSize As ULong, newMemSize As ULong
Dim memSizeChanged As Boolean = True
' Total amount of active threads in the process.
' This is the same as Process.ThreadCount
Dim oldThreadCount As UInteger, newThreadCount As UInteger
Dim threadCountChanged As Boolean = True
' Total amount of open handles owned by the process.
' This is the same as Process.HandleCount
Dim oldHandleCount As UInteger, newHandleCount As UInteger
Dim handleCountChanged As Boolean = True
' Total amount of data transferred by the process in I/O read operations.
Dim oldReadDataCount As ULong, newReadDataCount As ULong
Dim readDataChanged As Boolean = True
' Total amount of data transferred by the process in I/O write operations.
Dim oldWriteRateCount As ULong, newWriteDataCount As ULong
Dim writeDataChanged As Boolean = True
' WMI settings for process query.
Dim scope As New ManagementScope("root\CIMV2")
Dim query As New SelectQuery(String.Format("SELECT * FROM Win32_Process Where ProcessId = '{0}'", sender.Id))
Dim options As New EnumerationOptions With {.ReturnImmediately = True, .DirectRead = False}
Do
If (sender Is Nothing) OrElse (sender.HasExited) Then
Throw New InvalidOperationException(message:="The process is not running.")
Exit Sub
Else
sender.Refresh()
sender.WaitForInputIdle(Timeout.Infinite)
Using wmi As New ManagementObjectSearcher(scope, query, options)
Using obj As ManagementObject = wmi.Get().Cast(Of ManagementObject).SingleOrDefault()
If (obj IsNot Nothing) Then
newCpuTime = CULng(obj.Properties("KernelModeTime").Value)
cpuTimeChanged = (newCpuTime <> oldCpuTime)
oldCpuTime = newCpuTime
newMemSize = CULng(obj.Properties("PrivatePageCount").Value)
memSizeChanged = (newMemSize <> oldMemSize)
oldMemSize = newMemSize
newThreadCount = CUInt(obj.Properties("ThreadCount").Value)
threadCountChanged = (newThreadCount <> oldThreadCount)
oldThreadCount = newThreadCount
newHandleCount = CUInt(obj.Properties("HandleCount").Value)
handleCountChanged = (newHandleCount <> oldHandleCount)
oldHandleCount = newHandleCount
newReadDataCount = CULng(obj.Properties("ReadTransferCount").Value)
readDataChanged = (newReadDataCount <> oldReadDataCount)
oldReadDataCount = newReadDataCount
newWriteDataCount = CULng(obj.Properties("WriteTransferCount").Value)
writeDataChanged = (newWriteDataCount <> oldWriteRateCount)
oldWriteRateCount = newWriteDataCount
Debug.
WriteLine("CPU.Time........:{0}(ms) Memomy.Size.....:{1}(bytes)
Thread.Count....:{2}
Handle.Count....:{3}
Read..Data.Count:{4}
Write.Data.Count:{5}
".Replace(" "c, ""),
newCpuTime, newMemSize,
newThreadCount, newHandleCount,
newReadDataCount, newWriteDataCount)
#End If
Else
Continue Do
End If
End Using
End Using
If (cpuTimeChanged Or memSizeChanged Or
threadCountChanged Or handleCountChanged Or
readDataChanged Or writeDataChanged) = True Then
Thread.Sleep(TimeSpan.FromMilliseconds(checkInterval))
Continue Do
End If
End If
Loop Until (cpuTimeChanged Or memSizeChanged Or
threadCountChanged Or handleCountChanged Or
readDataChanged Or writeDataChanged) = False
End Sub
''' ----------------------------------------------------------------------------------------------------
''' <summary>
''' Aynchronouslly causes the <see cref="Process"/> component to wait indefinitely
''' until the user interface has been fully loaded then entered in IDLE state.
''' <para></para>
''' This applies only to processes with a user interface and, therefore, a message loop.
''' </summary>
''' ----------------------------------------------------------------------------------------------------
''' <param name="callback">
''' A <see cref="Action"/> delegate which will be invoked inmmediately
''' after the process is fully loaded and entered in IDLE state.
''' <para></para>
''' When <see cref="WaitForIdleAsync"/> method is called from a UI thread,
''' <paramref name="callback"/> is invoked on the same UI thread.
''' </param>
''' ----------------------------------------------------------------------------------------------------
<DebuggerStepThrough>
<Extension>
Public Sub WaitForIdleAsync(ByVal sender As Process,
ByVal callback As Action)
ProcessExtensions.WaitForIdleAsync(sender, callback, checkInterval:=1500)
End Sub
''' ----------------------------------------------------------------------------------------------------
''' <summary>
''' Aynchronouslly causes the <see cref="Process"/> component to wait for the specified amount of time, in milliseconds,
''' until the user interface has been fully loaded then entered in IDLE state.
''' <para></para>
''' This applies only to processes with a user interface and, therefore, a message loop.
''' </summary>
''' ----------------------------------------------------------------------------------------------------
''' <param name="callback">
''' A <see cref="Action"/> delegate that will be invoked inmmediately
''' after the process is fully loaded and entered in IDLE state.
''' <para></para>
''' When <see cref="WaitForIdleAsync"/> method is called from a UI thread,
''' <paramref name="callback"/> is invoked on the same UI thread.
''' </param>
'''
''' <param name="checkInterval">
''' The interval, in milliseconds, to check the parameters that determines
''' whether the user-interface has been loaded and the preocess entered in IDLE state.
''' <para></para>
''' It is recommended to experiment with a value between <c>1000</c> and <c>2000</c> ms,
''' smaller values could give unexpected results.
''' </param>
''' ----------------------------------------------------------------------------------------------------
<DebuggerStepThrough>
<Extension>
Public Sub WaitForIdleAsync(ByVal sender As Process,
ByVal callback As Action,
ByVal checkInterval As Integer)
Dim tScheduler As TaskScheduler
' If the current thread is a UI thread then run the callback synchronouslly on the UI thread...
If (Application.MessageLoop) Then
tScheduler = TaskScheduler.FromCurrentSynchronizationContext()
Else
tScheduler = TaskScheduler.Default
End If
Task.Factory.StartNew(Sub() ProcessExtensions.WaitForIdle(sender, checkInterval)).
ContinueWith(Sub(t) callback.Invoke(), CancellationToken.None, TaskContinuationOptions.None, tScheduler)
End Sub
End Module
Ejemplos de uso sincrónico:
Using p As New Process
p.StartInfo.FileName = "C:\Program Files\Photoshop\Photoshop.exe"
p.Start()
p.WaitForIdle(checkInterval:=1500)
End Using
MsgBox("Process's UI has been fully loaded.")
Ejemplos de uso asincrónico:
Using p As New Process
p.StartInfo.FileName = "C:\Program Files\Photoshop\Photoshop.exe"
p.Start()
p.WaitForIdleAsync(p, AddressOf WaitForIdleAsync_CallBack, checkInterval:=1500)
End Using
Private Sub WaitForIdleAsync_CallBack()
MsgBox("Process's UI has been fully loaded.")
End Sub
lo que me parece es que el programa mucho antes de que caer en un estado de tranquilidad ya hizo la carga todos los módulos que en mi caso con 100...
Como ya te dije no existe una ciencia con la que predecir la carga completa de la UI, yo te recomiendo usar un valor de entre 1000 a 2000 ms para el parámetro "checkInterval" de los códigos de arriba (en el código que tú pusiste de Stackoverflow también), si usas un valor como 100 ms no es nada bueno, creeme, si el PC está haciendo muchas tareas e intentas iniciar un proceso entonces la información del proceso (me refiero al objeto Process, y la información de WMI), tardará mucho más que 100 ms en actualizarse, y esto quiere decir que los valores que obtendrás en la siguiente iteración serán idénticos así que la comprobación de valores te podría dar un falso positivo, y te lo daría. En otras palabras, cuanto mayor sea el intervalo de ms que especifiques, más seguro será la comprobación.
Un saludo