6
\$\begingroup\$

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 Data As ADODB.Recordset
 Dim Output As String
 Dim ColumnLengths() As Long
 Set Data = New ADODB.Recordset
 Data.Fields.Append "ImageName", adVarChar, 255
 Data.Fields.Append "PID", adInteger, , adFldKeyColumn
 Data.Open
 Set WshShell = CreateObject("WScript.Shell")
 Set WshExec = WshShell.Exec("tasklist")
 Set StdOut = WshExec.StdOut
 Do While WshExec.Status = WshRunning
 If Not StdOut.AtEndOfStream Then
 Output = StdOut.ReadLine
 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.AddNew
 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
asked Feb 5, 2019 at 17:33
\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

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()
 Dim taskList As Collection
 Set taskList = EnumProcesses
 Dim task As Variant
 For Each task In taskList
 Debug.Print task.ImageName & ", " & task.MemUsage
 Next task
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 WshExec = WshShell.Exec("tasklist")
 Set StdOut = WshExec.StdOut
 Set Data = New Collection
 Do While WshExec.Status = WshRunning
 If Not StdOut.AtEndOfStream Then
 Output = StdOut.ReadLine
 Select Case True
 Case Len(Output) = 0, _
 Output Like "Image Name*"
 'Skip
 Case Output Like "====*"
 'Skip
 Case Else
 Dim thisTask As OSTask
 Set thisTask = New OSTask
 thisTask.Init Output
 Data.Add thisTask
 End Select
 End If
 Loop
 Set EnumProcesses = Data
End Function
answered Feb 6, 2019 at 19:40
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.