Process

會開始研究C++去控制Process的原因,是因為專案中用到的Selenium會無預警的Hang住,接著工作都無法繼續執行下去。在Linux的部分,我寫了一個Script去kill它。但Windows的部分該怎麼辦呢? 其實可以透過taskkill的指令。Selenium是透過Java去啟動執行的,因此它依賴於Java Process(java.exe)。但如果我們輸入taskkill /im java.exe,相信會發生你不想看到的事情。因此我們該如何取得Selenium的Java Process呢?其實只要能夠取得執行參數就能做到。小弟所購買的Windows程式設計,僅教導如何取得到Process列表。經過Google後,看到一套叫ProcessExplorer的軟體。
讓我篤信:一定可以辦到的。(OS: Google大神提供的方法很多都是C#的…) 最後終於讓我找到外國人寫的API,透過fetch process heap去取得command line。

GetNtProcessInfo所提供的方法BOOL sm_GetNtProcessInfo(const DWORD dwPID, smPROCESSINFO *ppi),我們必須提供一個pid給它。因此我們需要實作以下內容:

  1. 從所有process中取得java process list。
  2. 將每一個java process透過sm_GetNtProcessInfo去取得各別的cmd line。
  3. 取得cmd line中有出現selenium字眼的java process 。
  4. 刪除掉它!

以上步驟,我們可以學習到:

  1. 取得Process list。
  2. 如何使用sm_GetNtProcessInfo API。
  3. 刪除Process list。

看起來雖然才三個項目,但這之中蘊含著無數的精華!

ProcessUtil

首先我實作了一個ProcessUtil類別,它提供取得ProcEntry列表(getProcEntryList)、根據Process名稱取得ProcEntry列表(findProcEntryList)、根據pid取得ProcEntry(findProcEntry)與終結行程(terminateProcess)。以前在寫作業的時候,傳入字串的參數不是用string就是用char*。在看了Charles(這名字好眼熟?)的Windows程式設計後,得知Windows使用wchar去解決Unicode的問題。使用TCHAR是比較好的寫法,TCHAR的宣告可透過前處理器去決定用wchar還是char。像跨平台的程式碼,型態的宣告也會透過前處理器去決定。

#include "stdafx.h"
#include <list>
#include <map>
using namespace std;
 
class ProcessUtil {
public:
	static list<PROCESSENTRY32> getProcEntryList();
	static list<PROCESSENTRY32> findProcEntryList(_TCHAR* aProcName);
	static PPROCESSENTRY32 findProcEntry(DWORD aPid);
        static BOOL terminateProcess(DWORD aPid);
private:
	static map<DWORD,PROCESSENTRY32> getProcEntryWithPidMap();
};
getProcEntryWithPidMap將會回傳一個以pid為key,procEntry為value的map。實作原理是透過CreateToolhelp32Snapshot建立Process的Snapshot,接著透過Process32First找尋第一個Process,最後透過Process32Next將所有的Process資訊塞入map中。基本上書上或網路上找到的code都大同小異,這裡我把它做成map為了便於之後的search。
map<DWORD,PROCESSENTRY32> ProcessUtil::getProcEntryWithPidMap(){
	map<DWORD,PROCESSENTRY32> pidPeMap;
	HANDLE hProcessSnap = CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, 0 );
	if( hProcessSnap == INVALID_HANDLE_VALUE ) {
		return pidPeMap;
	}
 
	PROCESSENTRY32 pe32;
	pe32.dwSize = sizeof( PROCESSENTRY32 );
	if( !Process32First( hProcessSnap, &pe32 ) ){
		CloseHandle( hProcessSnap );          
		return pidPeMap;
	}
 
	do {
		pidPeMap.insert( pair<DWORD,PROCESSENTRY32>(pe32.th32ProcessID, pe32) );
	} while( Process32Next( hProcessSnap, &pe32 ) );
 
	CloseHandle( hProcessSnap );
	return pidPeMap;
}
剩下的三個method沒什麼特別的。getProcEntryList是透過getProcEntryWithPidMap實作;findProcEntryList透過wcscmp比對名稱;findProcEntry透過map去找到對應的value。這裡就和吃飯一樣容易。
list<PROCESSENTRY32> ProcessUtil::getProcEntryList(){
	list<PROCESSENTRY32> porcEntryList;
 
	map<DWORD,PROCESSENTRY32> pidPeMap = getProcEntryWithPidMap();
	map<DWORD,PROCESSENTRY32>::iterator it;
	for ( it=pidPeMap.begin() ; it != pidPeMap.end(); it++ ){
		porcEntryList.push_back((*it).second);
	}
 
	return porcEntryList;
}
 
list<PROCESSENTRY32> ProcessUtil::findProcEntryList(_TCHAR* aProcName){
	list<PROCESSENTRY32> porcEntryList;
 
	map<DWORD,PROCESSENTRY32> pidPeMap = getProcEntryWithPidMap();
	map<DWORD,PROCESSENTRY32>::iterator it;
	for ( it=pidPeMap.begin() ; it != pidPeMap.end(); it++ ){
		if(!wcscmp(aProcName, (*it).second.szExeFile)){
			porcEntryList.push_back((*it).second);
		}
	}
	return porcEntryList;
}
 
PPROCESSENTRY32 ProcessUtil::findProcEntry(DWORD aPid){
	map<DWORD,PROCESSENTRY32> pidPeMap = getProcEntryWithPidMap();
 
	map<DWORD,PROCESSENTRY32>::iterator it= pidPeMap.find(aPid);
	if( it == pidPeMap.end() )
		return NULL;
	return &it->second;
}
 
BOOL ProcessUtil::terminateProcess(DWORD aPid){
	HANDLE hProc = OpenProcess(PROCESS_TERMINATE, BOOL(0), aPid);
	if( hProc ) {
		TerminateProcess( hProc,0);
		return CloseHandle(hProc);
	}
	return false;
}

有了以上的code,我們就可以輕易的取到process的pid和終結行程。

Main

這裡實作以下幾個function:確認行程的Command line是否吻合(isMatchProcess)、終結所有名稱吻合的行程(terminalAllProcess)與終結所有名稱吻合且command line也吻合的行程(terminalMatchProcess)。基本上就是先透過ProcessUtil::findProcEntryList找到名稱吻合的行程列表,接著再透過參考文獻二的API去取得command line做比對。

#include "stdafx.h"
 
#include "stdafx.h"
#include "NTProcessInfo.h"
#include <vector>
#include <iostream>
#include <wchar.h>
using namespace std;
 
#include "ProcessUtil.h"
 
#ifdef _UNICODE
#define tcout wcout
#else
#define tcout cout
#endif
 
enum Action{
	EXIT, DEL_ALL, DEL_CHK
};
 
BOOL isMatchProcess(DWORD aPid, _TCHAR* aCmdLine){
	smPROCESSINFO procInfo;
	sm_GetNtProcessInfo( aPid, &procInfo);
	return wcswcs(procInfo.szCmdLine, aCmdLine) != 0;
}
 
void terminalAllProcess(list<PROCESSENTRY32> aProcList){
	list<PROCESSENTRY32>::iterator it;
	for ( it=aProcList.begin() ; it != aProcList.end(); it++ ){
		ProcessUtil::terminateProcess(it->th32ProcessID);
	}		
}
 
void terminalMatchProcess(list<PROCESSENTRY32> aProcList, _TCHAR* aCmdLine){
	list<PROCESSENTRY32>::iterator it;
	for ( it=aProcList.begin() ; it != aProcList.end(); it++ ){
		if( isMatchProcess(it->th32ProcessID, aCmdLine)) {
			tcout << "Terminal processes: " << it->th32ProcessID << endl;
			ProcessUtil::terminateProcess(it->th32ProcessID);
		}
	}		
}
 
int _tmain(int argc, _TCHAR* argv[])
{
	// check action.
	Action action;
	switch(argc){
	case 2:
		action = DEL_ALL;
		break;
	case 3:
		action = DEL_CHK;
		break;
	default:
		action = EXIT;
		break;
	}
 
	// exit
	if( action == EXIT ){
		return EXIT_SUCCESS;
	}
 
	// probe and delete process
	list<PROCESSENTRY32> foundProcList = ProcessUtil::findProcEntryList(argv[1]);
	int foundProcNumber = foundProcList.size();
	if( !foundProcNumber ){
		tcout << "Not found any process!" << endl;
		return EXIT_SUCCESS;
	} else {
		tcout << "Found " << foundProcNumber << " " << argv[1] << " processes." << endl;
	}
 
	if( action == DEL_ALL ) {
		tcout << "Terminal all processes." << endl;
		terminalAllProcess(foundProcList);
	} else if( action == DEL_CHK ){
		HMODULE hNtDll = sm_LoadNTDLLFunctions();
		if(!hNtDll){
			cout << "Load ntdll failed!" << endl;
			return EXIT_FAILURE;
		}
		sm_EnableTokenPrivilege(SE_DEBUG_NAME);
		terminalMatchProcess(foundProcList, argv[2]);
		sm_FreeNTDLLFunctions(hNtDll);
	}
 
	return EXIT_SUCCESS;
}
 
#include "NTProcessInfo.cpp"
這裡需要注意的是:在使用sm_GetNtProcessInfoLL之前,必須先透過sm_LoadNTDLLFunctions去Load NTDLL,接著再去EnableTokenPrivilege,最後用完要去清掉它。
	HMODULE hNtDll = sm_LoadNTDLLFunctions();
        if(!hNtDll){
		cout << "Load ntdll failed!" << endl;
		return EXIT_FAILURE;
	}
	sm_EnableTokenPrivilege(SE_DEBUG_NAME);
	terminalMatchProcess(foundProcList, argv[2]);
	sm_FreeNTDLLFunctions(hNtDll);

如下圖所示,輸入參數為java.exe selenium。四個java processes中,只會刪除與selenium有關的,執行結束後剩下三個java processes。

Download

其實參考文獻二中的程式碼,蘊含著系統程式的知識,如果有時間建議可以review一下。美中不足的是:要把它包進來是挺麻煩的一件事情,因為我最討厭去include cpp。
PS. 請別問我註解為什麼那麼少,套句Albert的話: 夠Agile的Team是不需要document的!

  1. Windows程式設計 - 王艷平 編著
  2. Windows程式設計 第五版 - Charles Petzold 原著, 余孟學 編譯