By this


2019-02-05 17:33:25 8 Comments

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

1 comments

@PeterT 2019-02-06 19:40:53

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

Related Questions

Sponsored Content

4 Answered Questions

4 Answered Questions

[SOLVED] General function to test for empty/no-value controls

  • 2017-09-14 20:32:01
  • DataWriter
  • 207 View
  • 3 Score
  • 4 Answer
  • Tags:   vba ms-access

3 Answered Questions

[SOLVED] A WinAPI C++ program for printing the master boot record of the hard drive

  • 2018-12-21 12:57:40
  • coderodde
  • 154 View
  • 5 Score
  • 3 Answer
  • Tags:   c++ winapi

1 Answered Questions

[SOLVED] SQL report of hazards for selected processes

0 Answered Questions

Exposing LocalDB API to VBA

0 Answered Questions

Rewriting the GnuWin32 Touch Command with the Win32 API

  • 2017-11-16 18:54:02
  • Jose Fernando Lopez Fernandez
  • 31 View
  • 3 Score
  • 0 Answer
  • Tags:   c++ file-system winapi

3 Answered Questions

[SOLVED] This macro will convert you... or not

  • 2017-05-23 14:06:58
  • svacxpython
  • 145 View
  • 4 Score
  • 3 Answer
  • Tags:   performance vba

1 Answered Questions

[SOLVED] A Moses's worthy string splitter

2 Answered Questions

[SOLVED] Win32 File API in VBA

  • 2015-08-28 19:57:38
  • Blackhawk
  • 1594 View
  • 14 Score
  • 2 Answer
  • Tags:   vba winapi

1 Answered Questions

[SOLVED] std::string/std::wstring template wrapper for Win32 API

Sponsored Content