Laufende Anwendungsprozesse mit Visual Basic 6 ermitteln

Veröffentlicht: 13. Okt 2005

Von Mathias Schiffer

Die Microsoft Newsgroups sind eine Quelle schier unerschöpflichen Wissens, das nahezu auf Knopfdruck abrufbar ist: Hunderte deutschsprachige Entwickler vom Einsteiger bis zum Profi treffen sich hier täglich virtuell, um Fragen zu stellen oder zu beantworten. Auch themennahe Probleme, Ansichten und Konzepte werden miteinander diskutiert. Sie kennen die Microsoft Newsgroups noch nicht? Detaillierte Information für Ihre Teilnahme finden Sie auf der Homepage der Microsoft Support Newsgroups.

Diese Kolumne greift regelmäßig ein besonders interessantes oder häufig nachgefragtes Thema aus einer der Entwickler-Newsgroups auf und arbeitet es aus.

Aus der Visual Basic Newsgroup microsoft.public.de.vb:

Ich möchte in Visual Basic 6 feststellen, welche Programme gerade ausgeführt werden. Dafür benötige ich die Dateinamen und -pfade. Ich benötige diese Information auch um zu prüfen, ob meine Anwendung schon ausgeführt wird.

Visual Basic vereinfacht es mit dem App-Objekt und seiner App.PrevInstance-Eigenschaft, nach dem Start einer VB-Anwendung festzustellen, ob es bereits eine vorherige Instanz davon gibt. Der Zugriff auf diese vorherige Instanz ist hingegen nicht ganz so einfach. Informationen, wie sie etwa der Taskmanager liefert, würden da bereits hilfreicher sein. Doch auch sonst gibt es diverse Gründe, die eine Liste der auf dem System laufenden Anwendungen notwendig erscheinen lassen. Dieser Beitrag zeigt das unter Windows 95 (inkl. dessen Nachfolgern 98 und Me) und Windows NT (inkl. dessen Nachfolgern 2000, XP, 2003 und Vista) unterschiedliche Vorgehen zur Zusammenstellung einer solchen Liste auf.

 

Prozessliste unter Windows 95 (und Nachfolgern)

Windows 95 und Windows 98 bieten für die Ermittlung einer solchen Liste in der Kernel32.dll die Funktionen CreateToolhelp32Snapshot, Process32First und Process32Next an, um mit einem "Snapshot" als Momentaufnahme Prozesse, Module von Prozessen und einige weitere Details erfassen zu können. Ist ein solcher "Snapshot" einmal angelegt, lässt er sich mithilfe von Process32First und Process32Next problemlos durch seine Einträge wiederholen, um die dort abgelegten Informationen zu ermitteln. Im Fall einer Momentaufnahme aktueller Prozesse ergeben sich als Iterationsergebnis die Prozess-ID des jeweiligen Prozesses sowie in einer PROCESSENTRY32-Struktur diverse weitere Informationen, von denen wir uns hier auf die Pfadangabe der zugehörigen Datei konzentrieren werden.

Ein Blick in den (leicht vereinfacht dargestellten) Sourcecode-Auszug des unten folgenden Beispielprojekts zeigt, wie unkompliziert das Vorgehen ist:

' Einen Snapshot der Prozessinformationen erstellen
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
If hSnap = 0 Then
  Exit Function ' Pech gehabt
End If
  
pEntry.dwSize = Len(pEntry) ' Größe der Struktur zur Verfügung stellen
  
' Den ersten Prozess im Snapshot ermitteln
ProcID = Process32First(hSnap, pEntry)
  
' Mittels Process32Next über alle weiteren Prozesse iterieren
Do While (ProcID <> 0) ' Gibt es eine gültige Prozess-ID?
  sName = TrimNullChar(pEntry.szExeFile)  ' Rückgabestring stutzen
  collProcesses.Add sName & "|" & CStr(ProcID) ' Collection-Eintrag
  ProcID = Process32Next(hSnap, pEntry)   ' Nächste PID des Snapshots
Loop
  
' Handle zum Snapshot freigeben
CloseHandle hSnap

 

Prozessliste unter Windows NT (und Nachfolgern)

Unter Windows NT hingegen wird das Vorgehen etwas komplizierter: Hier werden die erwünschten Informationen vom Betriebssystem im dynamischen, flüchtigen Teil der Registry unter dem symbolischen Schlüssel HKEY_PEFORMANCE_DATA abgelegt. Ein Zugriff auf diesen Teil der Registry ist zwar über die entsprechenden Registry-Funktionen des API möglich, gestaltet sich jedoch deutlich schwieriger als notwendig: Seit Windows 2000 gehört die Hilfsbibliothek Psapi.dll zum Standardumfang des Betriebssystems. Sie stellt Schnittstellen für den Zugriff auf diese Informationen zur Verfügung. Für frühere Versionen von Windows NT erhalten Sie die Psapi-Bibliothek, ursprünglich nur Bestandteil des Platform SDK für Windows NT, als Einzeldownload vom Microsoft Webserver.

Die Psapi.dll stellt für unseren Bedarf die selbst beschreibenden Funktionen EnumProcesses, EnumProcessModules und GetModuleFileNameEx zur Verfügung. Die Enum*-Funktionen benötigen für die Rückgabe von Prozess- bzw. Modul-IDs dabei jeweils ein Array des Datentyps Long in ausreichender Größe.

Zwar verraten die Funktionen nicht, welche Dimensionen für die Aufnahme aller IDs benötigt werden, informieren aber nach ihrem Aufruf über die Anzahl an Bytes, die in das angebotene Array geschrieben wurden. Hierdurch lässt sich eine ansteigende Redimensionierung mit folgendem erneutem Aufruf der Funktionen bis zum Erfolgsfall als Lösung etablieren: Ist die Anzahl geschriebener Bytes geringer als der dafür angebotene Platz, wurden alle verfügbaren IDs ermittelt.

Dim ProcessIDs() As Long
  
cb = 8         ' "CountBytes": Größe des Arrays (in Bytes)
cbNeeded = 9   ' cbNeeded muss initial größer als cb sein
  
' Schrittweise an die passende Größe des Prozess-ID-Arrays
' heranarbeiten. Dazu vergößern wir das Array großzügig immer
' weiter, bis der zur Verfügung gestellte Speicherplatz (cb)
' den genutzten (cbNeeded) überschreitet:
Do While cb <= cbNeeded ' Alle Bytes wurden belegt -
                        ' es könnten also noch mehr sein
  cb = cb * 2                      ' Speicherplatz verdoppeln
  ReDim ProcessIDs(cb / 4) As Long ' Long = 4 Bytes
  EnumProcesses ProcessIDs(1), cb, cbNeeded ' Array abholen
Loop

Ist diese Hürde genommen, liegen im Array ProcessIDs alle verfügbaren Prozess-IDs, gefolgt von den nicht belegten verbleibenden Array-Elementen unserer Redimensionierung. Da jedoch bekannt ist, dass jedes Element als Long 4 Bytes benötigt, lässt sich die tatsächliche Anzahl an IDs durch die Division des letzten Wertes von cbNeeded durch 4 ermitteln und so auf einfache Weise über alle gültigen Prozess-IDs iterieren.

Um aus den Prozess-IDs Handles auf die Prozesse erhalten zu können, muss man diese Prozesse mit OpenProcess öffnen. Hierbei sollte das geringste notwendige Zugriffsrecht (PROCESS_QUERY_INFORMATION + PROCESS_VM_READ) gewählt werden, um das Öffnen des Prozesses nicht an übertriebenen Anforderungen scheitern zu lassen. Dennoch lassen sich nicht sämtliche Prozesse des Systems öffnen - Anwendungsprozesse zumindest verweigern sich hier aber nicht.

hProcess = OpenProcess(PROCESS_QUERY_INFORMATION _
                    Or PROCESS_VM_READ, _
                       0, ProcessIDs(i))

Mit einem gültigen Handle auf den Prozess ist es nun möglich, die IDs seiner Module mit der Funktion EnumProcessModules zu ermitteln, deren zugehörige Dateien sodann über die Funktion GetModuleFileNameEx zur Verfügung stehen. Da alle Module des Prozesses derselben Datei entstammen, müssen wir uns um die Größe des Arrays diesmal keine Gedanken machen - bereits eine einzige Modul-ID reicht hierfür:

' EnumProcessModules gibt die dem Prozess angehörenden
' Module in einem Array zurück.
RetVal = EnumProcessModules(hProcess, Modules(1), _
                            1, cbNeeded2)
  
If (RetVal <> 0) Then ' EnumProcessModules war erfolgreich
  ModuleName = Space$(MAX_PATH) ' Speicher reservieren
  ' Den Pfadnamen für das erste gefundene Modul bestimmen
  LenName = GetModuleFileNameEx(hProcess, Modules(1), _
                                ModuleName, Len(ModuleName))
  ' Den gefundenen Pfad und die Prozess-ID unserer
  ' ProcessCollection hinzufügen (Trennzeichen "|")
  collProcesses.Add Left$(ModuleName, LenName) & "|" & _
                    CStr(ProcessIDs(i))
End If

Somit sind wir dank der Hilfe der Psapi.dll auch unter Windows NT (und Nachfolgern) auf vergleichsweise einfache Art am Ziel angekommen.

 

Finale: Ein Lösungsmodul für den Praxiseinsatz

Im abschließenden Sourcecode finden Sie ein komplettes Lösungsmodul vor, das alle angesprochenen Arbeiten sowohl für Windows 95 als auch für Windows NT (samt Nachfolgeversionen) anbietet. Zusätzlich finden Sie einige Servicefunktionen vor, mit denen Sie beispielsweise einen erkannten Prozess terminieren können (ganz im Stil der Prozessliste des Windows Taskmanagers). Die Erklärung der angebotenen Funktionen finden Sie im Eingangskommentar des Sourcecodes.

' -----------------------------------------------------------------
' Liste aktiver Anwendungsprozesse ermitteln
' Copyright © Mathias Schiffer 1999-2005
' -----------------------------------------------------------------
'
' KURZE FUNKTIONSBESCHREIBUNG:
'
' - Public Function GetProcessCollection() As Collection
'   Gibt eine String-Collection zurück, deren Einträge den
'   Aufbau "Prozessname|Prozess-ID" haben.
'
' - Public Function ProcessName(ByVal CollectionString As String) As String
'   Extrahiert aus einem String der Collection den Prozessnamen.
'
' - Public Function ProcessHandle(ByVal CollectionString As String) As Long
'   Extrahiert aus einem String der Collection die Prozess-ID.
'
' - Public Function KillProcessByPID(ByVal PID As Long) As Boolean
'   Terminiert einen Prozess auf Basis seiner Prozess-ID. Ein Prozess
'   sollte nur in "Notfällen" terminiert werden. Datenverluste der
'   terminierten Anwendung sind nicht ausgeschlossen.
'
' -----------------------------------------------------------------
  
Option Explicit
  
' ------------------------- DEKLARATIONEN -------------------------
  
' Deklaration notwendiger API-Funktionen:
  
' GetVersionEx dient der Erkennung des Betriebssystems:
Private Declare Function GetVersionEx _
  Lib "kernel32" Alias "GetVersionExA" ( _
  ByRef lpVersionInformation As OSVERSIONINFO _
  ) As Long
' Toolhelp-Funktionen zur Prozessauflistung (Win9x):
Private Declare Function CreateToolhelp32Snapshot _
  Lib "kernel32" ( _
  ByVal dwFlags As Long, _
  ByVal th32ProcessID As Long _
  ) As Long
Private Declare Function Process32First _
  Lib "kernel32" ( _
  ByVal hSnapshot As Long, _
  ByRef lppe As PROCESSENTRY32 _
  ) As Long
Private Declare Function Process32Next _
  Lib "kernel32" ( _
  ByVal hSnapshot As Long, _
  ByRef lppe As PROCESSENTRY32 _
  ) As Long
' PSAPI-Funktionen zur Prozessauflistung (Windows NT)
Private Declare Function EnumProcesses _
  Lib "psapi.dll" ( _
  ByRef lpidProcess As Long, _
  ByVal cb As Long, _
  ByRef cbNeeded As Long _
  ) As Long
Private Declare Function GetModuleFileNameEx _
  Lib "psapi.dll" Alias "GetModuleFileNameExA" ( _
  ByVal hProcess As Long, _
  ByVal hModule As Long, _
  ByVal ModuleName As String, _
  ByVal nSize As Long _
  ) As Long
Private Declare Function EnumProcessModules _
  Lib "psapi.dll" ( _
  ByVal hProcess As Long, _
  ByRef lphModule As Long, _
  ByVal cb As Long, _
  ByRef cbNeeded As Long _
  ) As Long
' Win32-API-Funktionen für Prozessmanagement
Private Declare Function OpenProcess _
  Lib "Kernel32.dll" ( _
  ByVal dwDesiredAccess As Long, _
  ByVal bInheritHandle As Long, _
  ByVal dwProcId As Long _
  ) As Long
Private Declare Function TerminateProcess _
  Lib "kernel32" ( _
  ByVal hProcess As Long, _
  ByVal uExitCode As Long _
  ) As Long
Private Declare Function CloseHandle _
  Lib "Kernel32.dll" ( _
  ByVal Handle As Long _
  ) As Long
  
' Deklaration notwendiger Konstanter:
  
Private Const MAX_PATH                  As Long = 260
Private Const PROCESS_QUERY_INFORMATION As Long = 1024
Private Const PROCESS_VM_READ           As Long = 16
Private Const STANDARD_RIGHTS_REQUIRED  As Long = &HF0000
Private Const SYNCHRONIZE               As Long = &H100000
Private Const PROCESS_ALL_ACCESS        As Long = STANDARD_RIGHTS_REQUIRED _
                                               Or SYNCHRONIZE Or &HFFF
Private Const TH32CS_SNAPPROCESS        As Long = &H2&
  
' Konstante für die Erkennung des Betriebssystems:
Private Const VER_PLATFORM_WIN32_NT     As Long = 2
  
' Notwendige Typdeklarationen
  
Private Type PROCESSENTRY32 ' Prozesseintrag
   dwSize As Long
   cntUsage As Long
   th32ProcessID As Long
   th32DefaultHeapID As Long
   th32ModuleID     As Long
   cntThreads As Long
   th32ParentProcessID As Long
   pcPriClassBase As Long
   dwFlags As Long
   szExeFile As String * MAX_PATH ' = 260
End Type
  
Private Type OSVERSIONINFO ' Betriebssystemerkennung
   dwOSVersionInfoSize As Long
   dwMajorVersion As Long
   dwMinorVersion As Long
   dwBuildNumber As Long
   dwPlatformId As Long
   szCSDVersion As String * 128
End Type
  
  
' ----------------------------- CODE ------------------------------
  
  
Public Function GetProcessCollection() As Collection
' Ermittelt die abfragbaren laufenden Prozesse des lokalen
' Rechners. Jeder gefundene Prozess wird mit seiner ID
' als String in einem Element der Rückgabe-Collection
' gespeichert im Format "Prozessname|Prozess-ID".
Dim collProcesses As New Collection
Dim ProcID As Long
  
  If (Not IsWindowsNT) Then
  
    ' WINDOWS 95 / 98 / Me
    ' --------------------
  
    Dim sName As String
    Dim hSnap As Long
    Dim pEntry As PROCESSENTRY32
  
    ' Einen Snapshot der Prozessinformationen erstellen
    hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
    If hSnap = 0 Then
      Exit Function ' Pech gehabt
    End If
  
    pEntry.dwSize = Len(pEntry) ' Größe der Struktur zur Verfügung stellen
  
    ' Den ersten Prozess im Snapshot ermitteln
    ProcID = Process32First(hSnap, pEntry)
  
    ' Mittels Process32Next über alle weiteren Prozesse iterieren
    Do While (ProcID <> 0) ' Gibt es eine gültige Prozess-ID?
      sName = TrimNullChar(pEntry.szExeFile)  ' Rückgabestring stutzen
      collProcesses.Add sName & "|" & CStr(ProcID) ' Collection-Eintrag
      ProcID = Process32Next(hSnap, pEntry)   ' Nächste PID des Snapshots
    Loop
  
    ' Handle zum Snapshot freigeben
    CloseHandle hSnap
  
  Else
  
    ' WINDOWS NT / 2000 / XP / 2003 / Vista
    ' -------------------------------------
  
    Dim cb As Long
    Dim cbNeeded As Long
    Dim RetVal As Long
    Dim NumElements As Long
    Dim ProcessIDs() As Long
    Dim cbNeeded2 As Long
    Dim NumElements2 As Long
    Dim Modules(1) As Long
    Dim ModuleName As String
    Dim LenName As Long
    Dim hProcess As Long
    Dim i As Long
  
    cb = 8         ' "CountBytes": Größe des Arrays (in Bytes)
    cbNeeded = 9   ' cbNeeded muss initial größer als cb sein
  
    ' Schrittweise an die passende Größe des Prozess-ID-Arrays
    ' heranarbeiten. Dazu vergößern wir das Array großzügig immer
    ' weiter, bis der zur Verfügung gestellte Speicherplatz (cb)
    ' den genutzten (cbNeeded) überschreitet:
    Do While cb <= cbNeeded ' Alle Bytes wurden belegt -
                            ' es könnten also noch mehr sein
      cb = cb * 2                      ' Speicherplatz verdoppeln
      ReDim ProcessIDs(cb / 4) As Long ' Long = 4 Bytes
      EnumProcesses ProcessIDs(1), cb, cbNeeded ' Array abholen
    Loop
  
    ' In cbNeeded steht der übergebene Speicherplatz in Bytes.
    ' Da jedes Element des Arrays als Long aus 4 Bytes besteht,
    ' ermitteln wir die Anzahl der tatsächlich übergebenen
    ' Elemente durch entsprechende Division:
    NumElements = cbNeeded / 4
  
    ' Jede gefundene Prozess-ID des Arrays abarbeiten
    For i = 1 To NumElements
  
      ' Versuchen, den Prozess zu öffnen und ein Handle zu erhalten
      hProcess = OpenProcess(PROCESS_QUERY_INFORMATION _
                          Or PROCESS_VM_READ, _
                             0, ProcessIDs(i))
  
      If (hProcess <> 0) Then ' OpenProcess war erfolgreich
  
        ' EnumProcessModules gibt die dem Prozess angehörenden
        ' Module in einem Array zurück.
        RetVal = EnumProcessModules(hProcess, Modules(1), _
                                    1, cbNeeded2)
  
        If (RetVal <> 0) Then ' EnumProcessModules war erfolgreich
          ModuleName = Space$(MAX_PATH) ' Speicher reservieren
          ' Den Pfadnamen für das erste gefundene Modul bestimmen
          LenName = GetModuleFileNameEx(hProcess, Modules(1), _
                                        ModuleName, Len(ModuleName))
          ' Den gefundenen Pfad und die Prozess-ID unserer
          ' ProcessCollection hinzufügen (Trennzeichen "|")
          collProcesses.Add Left$(ModuleName, LenName) & "|" & _
                            CStr(ProcessIDs(i))
        End If
  
      End If
  
      CloseHandle hProcess ' Offenes Handle schließen
  
    Next i
  
  End If
  
  ' Zusammengestellte Collection übergeben
  Set GetProcessCollection = collProcesses
  
End Function
  
  
Public Function ProcessName(ByVal CollectionString As String) As String
' Extrahiert aus einem String der Collection den Prozessnamen.
Dim Pos1 As Long
  
  ' Trenner suchen
  Pos1 = InStr(CollectionString, "|")
  ' Wenn Trenner vorhanden, Eintrag zurückgeben (sonst vbNullString)
  If (Pos1 > 0) Then
    ProcessName = Left$(CollectionString, Pos1 - 1)
  End If
  
End Function
  
  
Public Function ProcessHandle(ByVal CollectionString As String) As Long
' Extrahiert aus einem String der Collection die Prozess-ID.
Dim Pos1 As Long
  
  ' Trenner suchen
  Pos1 = InStr(CollectionString, "|")
  ' Wenn Trenner vorhanden, Handle zurückgeben (sonst 0)
  If (Pos1 > 0) And (Len(CollectionString) > Pos1) Then
    ProcessHandle = CLng(Mid$(CollectionString, Pos1 + 1))
  End If
  
End Function
  
  
Public Function KillProcessByPID(ByVal PID As Long) As Boolean
' Versucht auf Basis einer Prozess-ID, den zugehörigen
' Prozess zu terminieren. Im Erfolgsfall wird True zurückgegeben.
Dim hProcess As Long
  
  ' Öffnen des Prozesses über seine Prozess-ID
  hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, PID)
  
  ' Gibt es ein Handle, wird der Prozess darüber abgeschossen
  If (hProcess <> 0) Then
    KillProcessByPID = TerminateProcess(hProcess, 1&) <> 0
    CloseHandle hProcess
  End If
  
End Function
  
  
' ----------------- PRIVATE FUNKTIONEN ----------------------------
  
  
Private Function TrimNullChar(ByVal s As String) As String
' Kürzt einen String s bis zum Zeichen vor einem vbNullChar
Dim Pos1 As Long
  
  ' vbNullChar = Chr$(0) im String suchen
  Pos1 = InStr(s, vbNullChar)
  ' Falls vorhanden, den String entsprechend kürzen
  If (Pos1 > 0) Then
    TrimNullChar = Left$(s, Pos1 - 1)
  Else
    TrimNullChar = s
  End If
  
End Function
  
  
Private Function IsWindowsNT() As Boolean
' Gibt True für Windows NT (und 2000, XP, 2003, Vista) zurück
Dim OSInfo As OSVERSIONINFO
  
  With OSInfo
    .dwOSVersionInfoSize = Len(OSInfo)  ' Angabe der Größe dieser Struktur
    .szCSDVersion = Space$(128)         ' Speicherreservierung für Angabe des Service Packs
    GetVersionEx OSInfo                 ' OS-Informationen ermitteln
    IsWindowsNT = (.dwPlatformId = VER_PLATFORM_WIN32_NT) ' für Windows NT
  End With
  
End Function
  
  
' -----------------------------------------------------------------

Mathias Schiffer widmet sich als freier Technologievermittler und Entwickler großen Projekten ebenso wie arbeiterleichternden Alltagslösungen. Seit Jahren gibt er sein Wissen und seine Erfahrungen in unzähligen Publikationen und Beratungen auch an andere Entwickler und Entscheider weiter. Sie erreichen Mathias Schiffer per E-Mail an die Adresse Schiffer@mvps.org.