# Enumerating processes without enumerating flaws of the API

This arose out of the incident where it was realized that WMI could not be expected to be reliable across all various computers. The next thing I looked at was API which was positively awful to work with. Based on a suggestion, I decided to try and use tasklist. Note that efforts was made to ensure there are no disk I/Os, which avoids getting into the messy realms of file management. The only annoyance is that the WshExec will pop open a window but that can be managed and is beyond the scope of the question.

The question is - can we make the process more reliable and failsafe? The idea is that it must be consistent across several computer systems, Windows versions, and so on. This makes uses of Windows Host Scripting Model and ADODB recordset. The application already requires ADODB anyway and the code can be updated to be late-bound as well. For testing/development, I left this in early-bound state.

Public Function EnumProcesses() As ADODB.Recordset
Dim WshShell As IWshRuntimeLibrary.WshShell
Dim WshExec As IWshRuntimeLibrary.WshExec
Dim StdOut As IWshRuntimeLibrary.TextStream
Dim Output As String
Dim ColumnLengths() As Long

Data.Open

Set WshShell = CreateObject("WScript.Shell")
Set StdOut = WshExec.StdOut

Do While WshExec.Status = WshRunning
If Not StdOut.AtEndOfStream Then
Select Case True
Case Len(Output) = 0, _
Output Like "Image Name*"
'Skip
Case Output Like "====*"
Dim SplitColumns As Variant
SplitColumns = Split(Output, " ")
ReDim ColumnLengths(UBound(SplitColumns))

Dim i As Long
For i = 0 To UBound(SplitColumns)
ColumnLengths(i) = Len(SplitColumns(i))
Next
Case Else
Data.Fields("ImageName").Value = Mid$(Output, 1, ColumnLengths(0)) Data.Fields("PID").Value = Trim$(Mid$(Output, ColumnLengths(0) + 2, ColumnLengths(1))) Data.Update End Select End If Loop Set EnumProcesses = Data End Function  ## 1 Answer Apart from a performance question regarding ADODB recordsets, I only made one real change to your code. Since there are several fields that are output by the tasklist utility, I would want to capture all of that data just in case I need to expand my database at a later time. So I created a class called OSTask which accepts a single line from the tasklist output and parses it into its component parameters. (This means I could also skip the case you have to calculate column widths.) Class OSTask Option Explicit Private Type InternalData ImageName As String PID As Long SessionName As String 'could also be an Enum: Console, Services SessionNumber As Long MemUsage As Long End Type Private this As InternalData Public Property Get ImageName() As String ImageName = this.ImageName End Property Public Property Get PID() As Long PID = this.PID End Property Public Property Get SessionName() As String SessionName = this.SessionName End Property Public Property Get SessionNumber() As Long SessionNumber = this.SessionNumber End Property Public Property Get MemUsage() As Long MemUsage = this.MemUsage End Property Public Sub Init(ByVal taskData As String) '--- converts a single line output from the Windows command ' shell utility 'tasklist' and parses the data into the ' class properties Dim pos1 As Long Dim pos2 As Long '--- find the end of the task name, looking for double-space pos1 = InStr(1, taskData, " ", vbTextCompare) this.ImageName = Trim$(Left$(taskData, pos1)) '--- the next value is a number followed by a single space Dim i As Long For i = pos1 To Len(taskData) If Not Mid$(taskData, i, 1) = " " Then
pos2 = InStr(i, taskData, " ", vbTextCompare)
this.PID = CLng(Mid$(taskData, i, pos2 - i)) Exit For End If Next i '--- next value is the session name pos1 = pos2 + 1 pos2 = InStr(pos1, taskData, " ", vbTextCompare) this.SessionName = Trim$(Mid$(taskData, pos1, pos2 - pos1)) '--- the next value is a number followed by a single space For i = pos2 To Len(taskData) If Not Mid$(taskData, i, 1) = " " Then
pos2 = InStr(i, taskData, " ", vbTextCompare)
this.SessionNumber = CLng(Mid$(taskData, i, pos2 - i)) Exit For End If Next i '--- next value is the memory usage, a large number in thousands pos1 = pos2 pos2 = InStr(pos1, taskData, "K", vbTextCompare) Dim memUsageText As String memUsageText = Mid$(taskData, pos1, pos2 - pos1)
memUsageText = Replace\$(memUsageText, ",", vbNullString)
this.MemUsage = CLng(memUsageText) * 1000
End Sub


All of the properties are read-only in this case by design.

For my example, I converted your function to return a Collection rather than an ADODB.Recordset just to make my own testing simpler. So the only real change is in the Else case of the Select statement.

For my own learning purposes, I reviewed this answer's detailed review of the command shell interactions. Since you specifically stated that you are avoiding disk I/O, the option to pipe the output to a windows temp file is no good. To really prevent the command shell pop-up, you'd have to go with running a cscript under a wscript shell as the poster there indicates. Additionally, I couldn't find any historical information that the tasklist output has changed over time, so I believe your approach should remain viable across different Windows versions.

Here is my main module with my minor edits for testing:

Option Explicit

Sub test()

End Sub

Public Function EnumProcesses() As Collection
Dim WshShell As IWshRuntimeLibrary.WshShell
Dim WshExec As IWshRuntimeLibrary.WshExec
Dim StdOut As IWshRuntimeLibrary.TextStream
Dim Data As Collection
Dim Output As String
Dim ColumnLengths() As Long

Set WshShell = CreateObject("WScript.Shell")
Set StdOut = WshExec.StdOut

Set Data = New Collection

Do While WshExec.Status = WshRunning
If Not StdOut.AtEndOfStream Then
Select Case True
Case Len(Output) = 0, _
Output Like "Image Name*"
'Skip
Case Output Like "====*"
'Skip
Case Else