VC++定时检测进程,被检测进程挂掉后启动

一、应用场景

最近使用VC++写一个监控进程的后台程序,需要实现以下的功能:
1、遍历被监控的进程列表,定时(比如1分钟)检测进程列表,如果某个需要被监视的进程未运行,则启动它。
2、防止假死-使用心跳机制(UDP实现后台监控进程与被监控进程的心跳进制)
a、被检测进程定时,比如说每隔5秒给后台监控进程发送一个包含进程名称、进程所在绝对路径、发送心跳的时间戳等信息,后台监控进程定时检测遍历每个进程,如果该进程连续3次如15秒没有给监控进程发送心跳包,则说明该进程因为异常情况比如假死了,监控进程则先杀掉该进程然后再重启它。关于心跳包,使用JSON格式或者自定义的字符串格式都行,比如:@@AppName=UDPClientDemo;AppPath=E:\UDPClientDemo\UDPClientDemo.exe;CurrentTime=2020-05-26 10:29:00##
定义好头和尾部以及进程信息。
有些被检测程序死掉时,程序崩溃了,它就弹出一个错误对话框,如下图所示,并且如果不关掉掉这个框,程序就永远死在这个窗口上,不会退出,进程ID也不会变成0,那么就不能通过检测进程ID来判断程序是否崩溃。所以现在必须让程序崩溃后直接退出而不是死在错误窗口上。

pingjie.exe
b、被检测进程正常退出,不需要被检测时,可以给监控进程发送一条消息,说明该进程不需要被守护了。后台监控进程会在内存或者配置文件中维护一个被检测进程的映射表,将不需要检测的进程从该表中移除或者置某个字段(isDetected)为false即可。
为了简单起见,先编写一个基于Win32的控制台应用程序。

2、相关Windows VC++ API

(1)进程相关

VC++中启动外部进程即exe可执行程序,提供了三个SDK函数: WinExecShellExecuteACreateProcess

  • a、 WinExec
    WinExec的使用非常简单,其函数原型如下:
UINT WinExec(
  LPCSTR lpCmdLine,
  UINT   uCmdShow
);

其中,第一个参数是可执行exe程序的完整路径,比如D://QQ.exe,第二个参数为程序执行的显示方式,如SW_SHOW,SW_HIDE等。
如下测试程序如下:

WinExec("C:\\exams\\test.exe", SW_SHOW);

WinExec主要运行EXE文件。如:WinExec(“Notepad.exe Readme.txt”, SW_SHOW);
上面代码表示启动C:\exams目录下的test.exe可执行程序,并且显示窗口,封装成如下函数:

// 调用WinExec函数
	static void StartProgress(const char* fullPath)
	{
		// WinExec("C:\\exams\\test.exe", SW_SHOW);
		WinExec(fullPath, SW_SHOW);
	}

但是我在实际使用时,发现自己写的后台进程监控程序调用外部的exe进程时,不会显示窗口,有时候有些进程会在后台运行,不会显示窗口;并且退出后台监控进程后,对应的其他进程会退出,所以我在写后台监控进程时放弃了该函数。
微软的官方也不建议使用此函数:

This function is provided only for compatibility with 16-bit Windows. Applications should use the CreateProcess function.

建议使用CreateProcess ,WinExec函数提供仅仅用于为了兼容16位的Windows系统用的。

  • b、 ShellExecuteA
    ShellExecute不仅可以运行EXE文件,也可以运行已经关联的文件。
    ShellExecuteA函数的原型如下:
HINSTANCE ShellExecuteA(
  HWND   hwnd,
  LPCSTR lpOperation,
  LPCSTR lpFile,
  LPCSTR lpParameters,
  LPCSTR lpDirectory,
  INT    nShowCmd
);

Performs an operation on a specified file.
打开目录,使用如下之一的代码:

ShellExecute(handle, NULL, <fully_qualified_path_to_folder>, NULL, NULL, SW_SHOWNORMAL);

或者

ShellExecute(handle, "open", <fully_qualified_path_to_folder>, NULL, NULL, SW_SHOWNORMAL);

浏览目录,使用如下调用

ShellExecute(handle, "explore", <fully_qualified_path_to_folder>, NULL, NULL, SW_SHOWNORMAL);

运行Shell的目录查找工具,使用如下调用:

ShellExecute(handle, "find", <fully_qualified_path_to_folder>, NULL, NULL, 0);

开始一个新的应用程序
   ShellExecute(Handle, “open”, “c:\test\app.exe”, NULL, NULL, SW_SHOW);
打开某个外部exe可执行的函数接口可以封装成如下函数:

	// 启动某个外部程序
	// 需要提供绝对路径
	// 若xml配置文件中的APP是相对路径,则需要转换成绝对路径
	static void StartProgress(const char* fullPath)
	{
		ShellExecute(NULL, "open", fullPath, NULL, NULL, SW_SHOW);
	}

关于ShellExecute函数的详细使用,可以参考ShellExecute用法种种这篇CSD博客。

  • c、CreateProcess
    CreateProcess函数的参数比较多,使用稍微复杂些,其函数原型如下:
BOOL CreateProcess(
  LPCWSTR pszImageName,
  LPCWSTR pszCmdLine,
  LPSECURITY_ATTRIBUTES psaProcess,
  LPSECURITY_ATTRIBUTES psaThread,
  BOOL fInheritHandles,
  DWORD fdwCreate,
  LPVOID pvEnvironment,
  LPWSTR pszCurDir,
  LPSTARTUPINFOW psiStartInfo,
  LPPROCESS_INFORMATION pProcInfo
);

启动某个外部exe可执行的简单函数封装如下:

	// 使用CreateProcess启动某个外部进程
	static void RunProgress(const char* fullPath)
	{
		STARTUPINFO si;
		PROCESS_INFORMATION pi;

		ZeroMemory(&si, sizeof(si));
		si.cb = sizeof(si);
		si.dwFlags = STARTF_USESHOWWINDOW;
		//si.wShowWindow = SW_SHOWMINNOACTIVE;
		si.wShowWindow = SW_SHOW;
		ZeroMemory(&pi, sizeof(pi));

		// Start the child process. 
		if (!CreateProcess(NULL,   // No module name (use command line)
			(LPSTR)fullPath,       // Command line
			NULL,           // Process handle not inheritable
			NULL,           // Thread handle not inheritable
			FALSE,          // Set handle inheritance to FALSE
			0,              // No creation flags
			NULL,           // Use parent's environment block
			NULL,           // Use parent's starting directory 
			&si,            // Pointer to STARTUPINFO structure
			&pi)           // Pointer to PROCESS_INFORMATION structure
			)
		{
			printf("CreateProcess failed (%d).\n", GetLastError());
			// printf("启动失败的进程全路径为:%s\n", fullPath);
			return;
		}

		// Wait until child process exits.
		// WaitForSingleObject(pi.hProcess, INFINITE);
		WaitForSingleObject(pi.hProcess, 5);

		// Close process and thread handles. 
		CloseHandle(pi.hProcess);
		CloseHandle(pi.hThread);
	}

为了达到我想要的要求,直接使用ShellExecute函数了。
注意使用前需要引入如下头文件:

#include <Shellapi.h>

VC++控制台程序-进程后台监控程序

先使用VS2017等工具创建一个基于Win32控制台的项目,取名为:RestartOnCrashConsole
程序比较简单,使用xml作为配置文件,我选择了腾讯的pugixml库作为xml解析库,主要用于记录需要监控的应用程序信息(包括应用程序名称、路径(绝对路径或者相对路径),是否需要启动,启动下一个程序所需要的间隔时间)。

主入口程序

使用C++11的线程库std::thread,在main入口函数中开了一个监控线程,用于对Configure.xml文件中的应用程序配置信息进行读取,获取对应的应用程序列表后,每间隔15秒检测一次,然后启动需要启动的外部应用程序,可以使用相对路径或者绝对路径,但是路径一定要写对才行。

主程序RestartOnCrashConsole.cpp源代码如下:

// RestartOnCrashConsole.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include "pch.h"
#include <iostream>

#include "DateFormatter.h"
#include "ProgressHelper.h"
#include "Configure.h"
#include "Tools.h"

#include <thread>

// 控制线程是否运行的全局变量
volatile bool run_ = false;

// 监控线程处理函数
void monitorThreadFunc()
{
	run_ = true;

	while (run_)
	{
		if (!run_) break;

		// 每隔15秒检测需要启动的进程是否正在运行,若不再运行则重新启动它
		Configure conf;
		AppRow appList;
		// 从xml配置文件中获取所有的应用程序列表信息
		conf.getAppList(appList);

		// 遍历应用程序列表
		for (auto appIter = appList.begin(); appIter != appList.end(); ++appIter)
		{
			std::string strAppExeName = appIter->appName;
			// 如果应用程序名称中不包含后缀.exe,则添加上
			if (strAppExeName.find(".exe") == std::string::npos)
			{
				strAppExeName += ".exe";
			}
			// 将应用程序的相对路径转换成绝对路径
			std::string strFullPath = Tools::Files::abs_path(appIter->appPath);
			// 启动需要启动的进程
			if (appIter->isRun)
			{
				// 如果需要启动的应用程序处于未运行状态,则启动它
				if (!ProgressHelper::isExeRunning(strAppExeName.c_str()))
				{
					// 注意:这里调用ShellExecute启动外部应用程序,使用全路径(包括应用程序名称和扩展名.exe)
					ProgressHelper::StartProgress2(strFullPath.c_str());
					// 休眠waitTime秒
					ssleep(appIter->waitTime);
				}
			}
		}

		// 休眠15秒
		sleep_if(run_, 15);
	}
}

int main(int argc, char* argv[])
{
    // 启动后台监控线程,用于监控需要启动的进程
	std::thread thr = std::thread(monitorThreadFunc);
	
	// 等待用户按下任意键,退出程序
	getchar();

	// 退出后台监控进程
	run_ = false;
	
	// 等待后台监控子线程退出
	if (thr.joinable()) {
		thr.join();
	}

	return 0;
}

Configure类 应用程序xml配置类

  • 1、xml配置文件Configure.xml内容格式如下:
<?xml version="1.0" encoding="utf-8" ?>
<ConfigEntity>  
  <!--被守护程序-->
  <Apps>
    <!--配置说明
        name 必须为应用程序名称
        path 为守护程序相对路径或者绝对路径
        isRun 如需启动设置为true,否则为false
        waitTime 当前程序启动后等待时间,如redis启动后等待五秒执行下一程序
    -->
	<App name="Hubei212Recv" path ="..\Apps\Hubei212Recv\Hubei212Recv.exe" isRun="true" waitTime="1"/>  
	<App name="NetAssist-4.3.25" path ="..\Apps\NetAssist-4.3.25.exe" isRun="true" waitTime="1"/> 
	<App name="HBCityZlw" path ="E:\Apps\HBCityZlw.exe" isRun="true" waitTime="5"/> 
	<App name="HNCityZlw" path ="E:\Apps\HNCityZlw.exe" isRun="true" waitTime="5"/> 
  </Apps>
</ConfigEntity>
  • 2、对应的C++类Configure的头文件如下:
#pragma once
#define PUGIXML_HEADER_ONLY
#include "xml/pugixml.hpp"

#include "pub.h"

#include <mutex>

class Configure
{
public:
	Configure();
	~Configure();

	// 获取应用程序列表
	bool getAppList(AppRow& appList);
private:
	std::string m_xmlConfigPath;	// 配置文件所在路径
	static std::mutex m_mutex;		// 对XML文件读写进行加锁
};
  • 3、对应的C++类Configure的实现文件如下:
#include "pch.h"

#include "Configure.h"

std::mutex Configure::m_mutex;

Configure::Configure()
{
	m_xmlConfigPath = "./Configure.xml";
}

Configure::~Configure()
{

}

// 获取应用程序列表
bool Configure::getAppList(AppRow& appList)
{
	std::lock_guard<std::mutex> locker(m_mutex);

	pugi::xml_document doc;
	// 读取xml配置文件
	pugi::xml_parse_result rc = doc.load_file(m_xmlConfigPath.c_str());
	if (rc.status == pugi::status_ok)
	{
		auto appConf = doc.child("ConfigEntity").child("Apps");
		for (auto appIter = appConf.child("App"); appIter; appIter = appIter.next_sibling("App"))
		{
			// 获取应用程序信息
			AppItem appItem;
			// <App name = "Hubei212Recv" path = "..\Hubei212Recv\Hubei212Recv.exe" isRun = "false" waitTime = "1" / >
			appItem.appName = appIter.attribute("name").value();
			appItem.appPath = appIter.attribute("path").value();
			appItem.isRun = appIter.attribute("isRun").as_bool(true);
			appItem.waitTime = appIter.attribute("waitTime").as_uint(1);

			appList.push_back(appItem);
		}

		return true;
	}
	else if (rc.status == pugi::status_file_not_found)
	{
		printf("没有检测到配置文件,请新建配置文件. %s", rc.description());

		return false;
	}
	else
	{
		printf("配置文件存在错误,请检查配置. %s", rc.description());

		return false;
	}

	return false;
}

进程处理类 ProgressHelper

用于控制进程的启动、停止,查询某个进程是否处于运行状态
对应的头文件如下:

#pragma once

#include <Windows.h>
#include <Tlhelp32.h>
#include<comdef.h>

// 进程处理类
class ProgressHelper
{
public:
	// 判断指定进程名的进程是否存在
	// 判断某个进程是否正在运行
	static bool isExeRunning(const char *pProcessName)
	{
		HANDLE hProcessSnap;
		BOOL bRet = false;

		PROCESSENTRY32 pe32;
		// Take a snapshot of all processes in the system
		hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
		if (hProcessSnap == INVALID_HANDLE_VALUE)
		{
			bRet = false;
			return bRet;
		}
		// Set the size of the structure before using it.
		pe32.dwSize = sizeof(PROCESSENTRY32);
		// Retrieve information about the first process,
		// and exit if unsuccessful
		if (!Process32First(hProcessSnap, &pe32))
		{
			CloseHandle(hProcessSnap);
			bRet = false;
			return bRet;
		}
		// Now walk the snapshot of processes, and
		// display information about each process in turn
		while (Process32Next(hProcessSnap, &pe32))
		{
			if (strcmp(pProcessName, pe32.szExeFile) == 0)
			{
				return true;
			}
		}
		CloseHandle(hProcessSnap);

		return bRet;
	}

	// 根据进程名(如NotePad.exe)获取进程ID
	static DWORD GetProcessIdFromName(const char* processName)
	{
		PROCESSENTRY32 pe;
		DWORD id = 0;

		HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
		pe.dwSize = sizeof(PROCESSENTRY32);
		if (!Process32First(hSnapshot, &pe))
			return 0;
		char pname[300];
		do
		{
			pe.dwSize = sizeof(PROCESSENTRY32);
			if (Process32Next(hSnapshot, &pe) == FALSE)
				break;
			//比较两个字符串,如果找到了要找的进程
			if (strcmp(pname, processName) == 0)
			{
				id = pe.th32ProcessID;
				break;
			}

		} while (true);

		CloseHandle(hSnapshot);

		return id;
	}


	// 根据程序名关闭某个程序
	static void KillProgress(const char* exeName)
	{
		//通过进程名获取进程ID
		// 比如test.exe
		/*
			注意:
			在上面获取进程ID的时候,一定是"test.exe",只是exe程序的名字,没有路径!没有路径!没有路径!
			重要的事情说三遍!否则获取不到进程ID。
			但是WinExec打开的时候就要加上路径了,当然如果控制程序和exe程序在相同目录下,就不必啦。
		*/
		DWORD pid = GetProcessIdFromName(exeName);
		//获取进程的最大权限
		HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
		//关闭进程
		TerminateProcess(hProcess, 0);
		CloseHandle(hProcess);
	}

	// 调用WinExec函数
	static void StartProgress1(const char* fullPath)
	{
		// WinExec("C:\\exams\\test.exe", SW_SHOW);
		WinExec(fullPath, SW_SHOW);
	}

	// 启动某个外部程序
	// 需要提供绝对路径
	// 若xml配置文件中的APP是相对路径,则需要转换成绝对路径
	static void StartProgress2(const char* fullPath)
	{
		ShellExecute(NULL, "open", fullPath, NULL, NULL, SW_SHOW);
	}

	// 使用CreateProcess启动某个外部进程
	static void StartProgress3(const char* fullPath)
	{
		STARTUPINFO si;
		PROCESS_INFORMATION pi;

		ZeroMemory(&si, sizeof(si));
		si.cb = sizeof(si);
		si.dwFlags = STARTF_USESHOWWINDOW;
		//si.wShowWindow = SW_SHOWMINNOACTIVE;
		si.wShowWindow = SW_SHOW;
		ZeroMemory(&pi, sizeof(pi));

		// Start the child process. 
		if (!CreateProcess(NULL,   // No module name (use command line)
			(LPSTR)fullPath,       // Command line
			NULL,           // Process handle not inheritable
			NULL,           // Thread handle not inheritable
			FALSE,          // Set handle inheritance to FALSE
			0,              // No creation flags
			NULL,           // Use parent's environment block
			NULL,           // Use parent's starting directory 
			&si,            // Pointer to STARTUPINFO structure
			&pi)           // Pointer to PROCESS_INFORMATION structure
			)
		{
			printf("CreateProcess failed (%d).\n", GetLastError());
			printf("启动失败的进程全路径为:%s\n", fullPath);
			return;
		}

		// Wait until child process exits.
		// WaitForSingleObject(pi.hProcess, INFINITE);
		WaitForSingleObject(pi.hProcess, 5);

		// Close process and thread handles. 
		CloseHandle(pi.hProcess);
		CloseHandle(pi.hThread);
	}
};

其他辅助类

  • 日期时间相关 DateFormatter.h
#pragma once
#include <chrono>
using namespace std::chrono;
// 毫秒时钟
typedef time_point<system_clock, milliseconds> millisecClock_type;

class DateFormatter
{
public:
	static time_t now_s() {
		return system_clock::to_time_t(system_clock::now());
	}
	static time_t now_ms() {
		return time_point_cast<milliseconds>(system_clock::now()).time_since_epoch().count();
		// return system_clock::to_time_t(time_point_cast<milliseconds>(system_clock::now()));
	}
};

#include <thread>
typedef std::thread Thread;

#include <chrono>
// 休眠毫秒
#define msleep(millsec) std::this_thread::sleep_for(std::chrono::milliseconds(millsec));
// 休眠秒
#define ssleep(sec) std::this_thread::sleep_for(std::chrono::seconds(sec));

// 条件休眠,单位秒
static inline void sleep_if(volatile bool& condition, int sec) {
	auto t0 = DateFormatter::now_s();
	while (condition && (DateFormatter::now_s() - t0) < sec)
	{
		msleep(20);
	}
}
// 条件休眠,单位毫秒
static inline void usleep_if(volatile bool& condition, int millsec) {
	auto t0 = DateFormatter::now_ms();
	while (condition && (DateFormatter::now_ms() - t0) < millsec)
	{
		msleep(20);
	}
}
  • Tools.h
#pragma once

#include <string>
#include <direct.h>
#include <stdlib.h>
#include  <limits.h>
#ifdef _WIN32
#include <windows.h>
#include <direct.h>
#include <io.h>
#else
#include<unistd.h>
#endif

namespace Tools
{
	class Files
	{
	public:
		static  std::string abs_path(std::string path)
		{
#ifdef _WIN32
#define max_path 4096
			char resolved_path[max_path] = { 0 };
			_fullpath(resolved_path, path.c_str(), max_path);
#else
			//linux release有个坑,需要大点的空间
#define max_path 40960
			char resolved_path[max_path] = { 0 };
			realpath(path.c_str(), resolved_path);
#endif
			return std::string(resolved_path);
		}
	};
}

pub.h

#pragma once

#include <string>
#include <vector>
#include <map>

struct AppItem
{
	std::string appName;			// 应用程序名称
	std::string appPath;			// 守护程序相对路径
	bool isRun;						// 是否需要启动,如需启动设置为true,否则为false
	unsigned int waitTime;			// 当前程序启动后等待时间,如redis启动后等待五秒执行下一程序
};

typedef std::vector<AppItem> AppRow;
typedef std::map<std::string, AppRow> AppMap;

程序目录结构如下图所示:
RestartOnCrashConsole

运行程序

编译生可执行程序RestartOnCrashConsole.exe后,将Configure.xml放在同一目录,Configure.xml内容如下:

<?xml version="1.0" encoding="utf-8" ?>
<ConfigEntity>  
  <!--被守护程序-->
  <Apps>
    <!--配置说明
        name 必须为应用程序名称
        path 为守护程序相对路径或者绝对路径
        isRun 如需启动设置为true,否则为false
        waitTime 当前程序启动后等待时间,如redis启动后等待五秒执行下一程序
    -->
	<App name="Hubei212Recv" path ="..\Apps\Hubei212Recv\Hubei212Recv.exe" isRun="true" waitTime="1"/>  
	<App name="NetAssist-4.3.25" path ="..\Apps\NetAssist-4.3.25.exe" isRun="true" waitTime="1"/> 
	<App name="HBCityZlw" path ="E:\SoftDevelop\CPlus\VCProjets\RestartOnCrashConsole\Apps\HBCityZlw.exe" isRun="true" waitTime="5"/> 
	<App name="HNCityZlw" path ="E:\SoftDevelop\CPlus\VCProjets\RestartOnCrashConsole\Apps\HNCityZlw.exe" isRun="true" waitTime="5"/> 
  </Apps>
</ConfigEntity>

然后在对应的目录下放置如上一些可执行程序,目录要写对,不论是相对路径还是绝对路径,如下图所示:
222
333
444
所有工作做好后,直接双击运行E:\SoftDevelop\CPlus\VCProjets\RestartOnCrashConsole\Debug目录下的RestartOnCrashConsole.exe可执行程序,它会定时监控同级目录下Configure.xml文件中的应用列表Apps,启动其中每个App中isRun为true的可执行程序。运行效果图如下:
666

源代码下载

为了方便,我把对应的源代码托管到码云RestartOnCrashConsole上。
使用git clone获取源代码

git clone https://gitee.com/havealex/RestartOnCrashConsole.git

三、相关软件 Restart on Crash 1.6.3.0

从网上搜到老外写的一款软件,看样式应该是基于QT写的,名字为:RestartOnCrash,软件对应的下载地址为:Download Restart on Crash (1.4 MB)

RestartOnCrash是一个可移植的免费实用程序,它监视用户选择的应用程序,并将自动重新启动任何挂起或崩溃的应用程序。

它使您可以添加任何数量的应用程序进行监视,并可以单独启用/禁用它们,并可以在重新启动应用程序时编辑命令行以供使用。它具有可移植性,因此您可以将其保存在USB驱动器上,以便在旅途中利用它,以最少的用户操作即可保持应用程序的正常运行。

从崩溃的重启是从一个超级干净和简约的界面提供的,该界面在使用时不会消耗太多资源。使用“崩溃时重新启动”很简单;它通过允许您选择特定的EXE文件来监视应用程序。此外,它还列出了当前正在运行的进程,并允许您从列表中进行选择,作为添加监视项的另一种方法。您还可以选择指导它执行自定义命令,以杀死崩溃后仍在运行的进程,并且可以针对每个项目进行配置。重新启动Crash还会生成一个日志文件,该日志文件会记录所有可能的错误。此应用程序的简单性使其成为遇到周期性应用程序崩溃并希望自动重新启动它们的用户的任何技能水平的绝佳选择。
软件界面如下图所示:
RestartOnCrash

四、参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值