@levinzhang
2023-02-26T13:20:30.000000Z
字数 18835
阅读 645
by
在微软Windows操作系统中,Windows服务发挥着重要的作用,能够创建和管理长时间运行的进程。但是,当启用“Fast Startup”时,在正常关机并启动计算机后,服务可能无法重新启动。本文的目标在于创建一个持久化的服务,当Windows重启或关闭后,服务始终能够运行并重启。
当使用C++为Windows编程时,使用Windows服务(Windows Services)几乎是难以避免的。在微软Windows操作系统中,Windows服务发挥着重要的作用,它们能够创建和管理长时间运行的进程,这些进程能够在睡眠、休眠、重启和关机的过程中幸存下来。但是,如果无法做到这一点会怎样呢?在选中快速启动(Fast Startup)时,关闭计算机会导致服务无法重启,这会给程序带来灾难性的后果。微软在Windows Vista中引入的Service Isolation可能会导致这类灾难性的后果,在本文中将会阐述如何解决它。
多年以来,我们一直在使用Windows服务,但是不管我们觉得有多么了解服务,或者有多么自信能够处理它,却始终会遇到更多的问题、挑战和麻烦。其中有些问题根本是没有文档的,或者我们“幸运”一点的话,会有一点糟糕的文档。
自从微软引入服务隔离之后,我们遇到的最令人恼火的问题之一就是当快速启动选中时,计算机关闭后,无法重启服务。鉴于我们没有找到现成的解决方案,所以我们决定自动动手实现一个,这促成了持久化服务的开发。
但是,在深入研究和解释我们的解决方案之前,我们首先从基础知识开始,解释什么是服务,以及为什么要使用Windows服务。
NT服务(也叫做Windows服务)指的是由NT内核的服务控制管理器(Service Control Manager)加载的特殊进程,它会在Windows启动(在用户登录前)立即在后台运行。我们使用服务来执行核心和底层的操作系统任务,比如Web服务、事件日志、文件服务、帮助和支持、打印、加密和错误报告。
此外,服务使我们能够创建可执行的、长时间运行的应用程序。原因在于服务会在自己的Windows会话环境中运行,所以它不会干扰应用程序的其他组件或会话。显然,我们期望服务会在计算机启动后也自动启动,我们马上就会讨论该问题。
进一步来讲,这里显然有一个问题:我们为什么需要持久化的服务?答案很明显,服务应该能够:
Windows服务需要能够在睡眠、休眠、重启和关机时依然能够存活。但是,正如前文所述,当选中“快速启动”时,计算机关机再启动的话,会出现一些特定的危险问题。在大多数场景中,服务无法重新启动。
因为我们正在开发的是一个反病毒软件,它应该在重启或关机后重新启动,这种情况造成了一个严重的问题,我们迫切需要解决它。
为了创建近乎完美的持久化Windows服务,我们必须首先解决几个底层的问题。
其中一个问题与服务隔离有关,被隔离的服务无法访问与任何特定用户相关的上下文。我们某个软件产品将数据存储到了c:\users\<USER NAME>\appdata\local\中,但是当它从我们的服务中运行的话,这个路径就是无效的,因为服务是在Session 0中运行的。除此之外,在重启后,服务会在所有用户登录之前启动,这形成了解决方案的第一部分:等待用户登录。
为了弄清如何做到这一点,我们在这里发布了遇到的问题。
事实证明,这是一个没有完美解决方案的问题,但是,本文附带的代码已经得到了应用,并且经过了全面的测试,没有任何的问题。
我们的代码结构和流程可能看起来很复杂,但是这是有一定原因的。在过去的十年间,服务已经与其他进程隔离。从那时开始,Windows服务会在SYSTEM用户账号下运行,而不是其他的用户账号,并且是隔离运行的。
隔离运行的原因在于,服务的功能很强大,可能是潜在的安全风险。正因为如此,微软引入了服务隔离。在这个变化之前,所有的服务会与应用一起在Session 0中运行。
但是,在引入了隔离之后(这是在Windows Vista中引入的),情况发生了变化。我们的代码背后的想法是通过调用CreateProcessAsUserW,让Windows服务以某个用户的身份启动自己,这一点将在后文详细阐述。我们的服务叫做SG_RevealerService,它有多个命令,当使用如下的命令行参数调用时,它们会采取相应的行为。
#define SERVICE_COMMAND_INSTALL L"Install" // The command line argument// for installing the service#define SERVICE_COMMAND_LAUNCHER L"ServiceIsLauncher" // Launcher command for// NT service
当调用SG_RevealerService时,有三个选项:
选项1:不带有任何命令行参数进行调用。在这种情况下什么都不会发生。
选项2:带有Install命令行参数进行调用。在这种情况下,服务将自行安装,如果在哈希分隔符(#)添加了有效的可执行路径,服务将会启动,Windows看门狗会保持其一直运行。
然后,Service会使用CreateProcessAsUserW()运行自身,新的进程会在用户账号下运行。这给了Service访问上下文的能力,因为Service Isolation,调用实例是无法访问该上下文的。
选项3:使用ServiceIsLauncher命令行参数进行调用。服务客户端主应用将会启动。此时,入口函数表明服务已经以当前用户的权限启动了自身。现在,在Task Manager中,我们会看到SG_RevealerService的两个实例,其中一个在SYSTEM用户下,另一个在当前登录用户下。
/filters:no_upscale()/articles/windows-services-reliable-restart/en/resources/1figure-1-task-manager-1671630831799.jpg)
/*RunHost*/BOOL RunHost(LPWSTR HostExePath,LPWSTR CommandLineArguments){WriteToLog(L"RunHost '%s'",HostExePath);STARTUPINFO startupInfo = {};startupInfo.cb = sizeof(STARTUPINFO);startupInfo.lpDesktop = (LPTSTR)_T("winsta0\\default");HANDLE hToken = 0;BOOL bRes = FALSE;LPVOID pEnv = NULL;CreateEnvironmentBlock(&pEnv, hToken, TRUE);PROCESS_INFORMATION processInfoAgent = {};PROCESS_INFORMATION processInfoHideProcess = {};PROCESS_INFORMATION processInfoHideProcess32 = {};if (PathFileExists(HostExePath)){std::wstring commandLine;commandLine.reserve(1024);commandLine += L"\"";commandLine += HostExePath;commandLine += L"\" \"";commandLine += CommandLineArguments;commandLine += L"\"";WriteToLog(L"launch host with CreateProcessAsUser ... %s",commandLine.c_str());bRes = CreateProcessAsUserW(hToken, NULL, &commandLine[0],NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS |CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE |CREATE_DEFAULT_ERROR_MODE, pEnv,NULL, &startupInfo, &processInfoAgent);if (bRes == FALSE){DWORD dwLastError = ::GetLastError();TCHAR lpBuffer[256] = _T("?");if (dwLastError != 0) // Don't want to see an// "operation done successfully" error ;-){::FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, // It's a system errorNULL, // No string to be// formatted neededdwLastError, // Hey Windows: Please// explain this error!MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Do it in the standard// languagelpBuffer, // Put the message here255, // Number of bytes to store the messageNULL);}WriteToLog(L"CreateProcessAsUser failed - Command Line = %s Error : %s",commandLine, lpBuffer);}else{if (!writeStringInRegistry(HKEY_LOCAL_MACHINE,(PWCHAR)SERVICE_REG_KEY, (PWCHAR)SERVICE_KEY_NAME, HostExePath)){WriteToLog(L"Failed to write registry");}}}else{WriteToLog(L"RunHost failed because path '%s' does not exists", HostExePath);}hPrevAppProcess = processInfoAgent.hProcess;CloseHandle(hToken);WriteToLog(L"Run host end!");return bRes;}
第一个挑战是仅在用户登录时,才启动一些动作。为了探测用户的登录,我们首先定义一个全局变量。
bool g_bLoggedIn = false;
当用户登录时,它的值应该被设置为true。
我们定义了如下的Preprocesor Directives:
#define EVENT_SUBSCRIBE_PATH L"Security"#define EVENT_SUBSCRIBE_QUERY L"Event/System[EventID=4624]"
当Service启动后,我们订阅登录事件,所以当用户登录时,我们会通过设置的回调函数得到一个告警,然后我们就可以继续后面的操作了。为了实现这一点,我们需要一个类来处理订阅的创建并等待事件回调。
class UserLoginListner{HANDLE hWait = NULL;HANDLE hSubscription = NULL;public:~UserLoginListner(){CloseHandle(hWait);EvtClose(hSubscription);}UserLoginListner(){const wchar_t* pwsPath = EVENT_SUBSCRIBE_PATH;const wchar_t* pwsQuery = EVENT_SUBSCRIBE_QUERY;hWait = CreateEvent(NULL, FALSE, FALSE, NULL);hSubscription = EvtSubscribe(NULL, NULL,pwsPath, pwsQuery,NULL,hWait,(EVT_SUBSCRIBE_CALLBACK)UserLoginListner::SubscriptionCallback,EvtSubscribeToFutureEvents);if (hSubscription == NULL){DWORD status = GetLastError();if (ERROR_EVT_CHANNEL_NOT_FOUND == status)WriteToLog(L"Channel %s was not found.\n", pwsPath);else if (ERROR_EVT_INVALID_QUERY == status)WriteToLog(L"The query \"%s\" is not valid.\n", pwsQuery);elseWriteToLog(L"EvtSubscribe failed with %lu.\n", status);CloseHandle(hWait);}}
然后,我们需要一个函数实现等待:
void WaitForUserToLogIn(){WriteToLog(L"Waiting for a user to log in...");WaitForSingleObject(hWait, INFINITE);WriteToLog(L"Received a Logon event - a user has logged in");}
我们还需要一个回调函数:
static DWORD WINAPI SubscriptionCallback(EVT_SUBSCRIBE_NOTIFY_ACTION action, PVOIDpContext, EVT_HANDLE hEvent){if (action == EvtSubscribeActionDeliver){WriteToLog(L"SubscriptionCallback invoked.");HANDLE Handle = (HANDLE)(LONG_PTR)pContext;SetEvent(Handle);}return ERROR_SUCCESS;}
接下来,需要做的就是添加具有如下内容的代码块:
WriteToLog(L"Launch client\n"); // launch client ...{UserLoginListner WaitTillAUserLogins;WaitTillAUserLogins.WaitForUserToLogIn();}
到达代码块的底部时,我们就可以确信一个用户已经登录了。
在本文后面的内容中,我们将会介绍如何检索登录用户的账号/用户名,以及如何使用GetLoggedInUser()函数。
当确定一个用户已经登录时,我们需要冒充他们。
如下的函数完成了这项工作。它不仅冒充了用户,还调用了CreateProcessAsUserW(),以该用户的身份运行自己。通过这种方式,我们能够让服务访问用户的上下文,包括文档、桌面等,并允许服务使用用户界面,这对于从Session 0运行服务来讲是无法实现的。
CreateProcessAsUserW创建了一个新的进程及其主线程,它会在给定用户的上下文中运行。
//Function to run a process as active user from Windows servicevoid ImpersonateActiveUserAndRun(){DWORD session_id = -1;DWORD session_count = 0;WTS_SESSION_INFOW *pSession = NULL;if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSession, &session_count)){WriteToLog(L"WTSEnumerateSessions - success");}else{WriteToLog(L"WTSEnumerateSessions - failed. Error %d",GetLastError());return;}TCHAR szCurModule[MAX_PATH] = { 0 };GetModuleFileName(NULL, szCurModule, MAX_PATH);for (size_t i = 0; i < session_count; i++){session_id = pSession[i].SessionId;WTS_CONNECTSTATE_CLASS wts_connect_state = WTSDisconnected;WTS_CONNECTSTATE_CLASS* ptr_wts_connect_state = NULL;DWORD bytes_returned = 0;if (::WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE,session_id,WTSConnectState,reinterpret_cast<LPTSTR*>(&ptr_wts_connect_state),&bytes_returned)){wts_connect_state = *ptr_wts_connect_state;::WTSFreeMemory(ptr_wts_connect_state);if (wts_connect_state != WTSActive) continue;}else{continue;}HANDLE hImpersonationToken;if (!WTSQueryUserToken(session_id, &hImpersonationToken)){continue;}//Get the actual token from impersonation oneDWORD neededSize1 = 0;HANDLE *realToken = new HANDLE;if (GetTokenInformation(hImpersonationToken, (::TOKEN_INFORMATION_CLASS) TokenLinkedToken, realToken, sizeof(HANDLE), &neededSize1)){CloseHandle(hImpersonationToken);hImpersonationToken = *realToken;}else{continue;}HANDLE hUserToken;if (!DuplicateTokenEx(hImpersonationToken,TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS | MAXIMUM_ALLOWED,NULL,SecurityImpersonation,TokenPrimary,&hUserToken)){continue;}// Get user name of this processWCHAR* pUserName;DWORD user_name_len = 0;if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session_id, WTSUserName, &pUserName, &user_name_len)){//Now we got the user name stored in pUserName}// Free allocated memoryif (pUserName) WTSFreeMemory(pUserName);ImpersonateLoggedOnUser(hUserToken);STARTUPINFOW StartupInfo;GetStartupInfoW(&StartupInfo);StartupInfo.cb = sizeof(STARTUPINFOW);PROCESS_INFORMATION processInfo;SECURITY_ATTRIBUTES Security1;Security1.nLength = sizeof SECURITY_ATTRIBUTES;SECURITY_ATTRIBUTES Security2;Security2.nLength = sizeof SECURITY_ATTRIBUTES;void* lpEnvironment = NULL;// Obtain all needed necessary environment variables of the logged in user.// They will then be passed to the new process we create.BOOL resultEnv = CreateEnvironmentBlock(&lpEnvironment, hUserToken, FALSE);if (!resultEnv){WriteToLog(L"CreateEnvironmentBlock - failed. Error %d",GetLastError());continue;}std::wstring commandLine;commandLine.reserve(1024);commandLine += L"\"";commandLine += szCurModule;commandLine += L"\" \"";commandLine += SERVICE_COMMAND_Launcher;commandLine += L"\"";WCHAR PP[1024]; //path and parametersZeroMemory(PP, 1024 * sizeof WCHAR);wcscpy_s(PP, commandLine.c_str());// Next we impersonate - by starting the process as if the current logged in user, has started itBOOL result = CreateProcessAsUserW(hUserToken,NULL,PP,NULL,NULL,FALSE,NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,NULL,NULL,&StartupInfo,&processInfo);if (!result){WriteToLog(L"CreateProcessAsUser - failed. Error %d",GetLastError());}else{WriteToLog(L"CreateProcessAsUser - success");}DestroyEnvironmentBlock(lpEnvironment);CloseHandle(hImpersonationToken);CloseHandle(hUserToken);CloseHandle(realToken);RevertToSelf();}WTSFreeMemory(pSession);}
为了寻找已登录用户的账号名,我们会使用如下的函数:
std::wstring GetLoggedInUser(){std::wstring user{L""};WTS_SESSION_INFO *SessionInfo;unsigned long SessionCount;unsigned long ActiveSessionId = -1;if(WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE,0, 1, &SessionInfo, &SessionCount)){for (size_t i = 0; i < SessionCount; i++){if (SessionInfo[i].State == WTSActive ||SessionInfo[i].State == WTSConnected){ActiveSessionId = SessionInfo[i].SessionId;break;}}wchar_t *UserName;if (ActiveSessionId != -1){unsigned long BytesReturned;if (WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE,ActiveSessionId, WTSUserName, &UserName, &BytesReturned)){user = UserName; // Now we have the logged in user nameWTSFreeMemory(UserName);}}WTSFreeMemory(SessionInfo);}return user;}
在服务启动后不久,我们就要使用该函数。只要没有用户登录,这个函数就会返回一个空字符串,如果这样的话,我们就知道应该继续等待。
Service与看门狗机制协同使用是很理想的方案。
这种机制将确保一个给定应用始终处于运行状态,如果它异常关闭的话,看门狗会重新启动它。我们要始终记住,如果用户通过Quit退出的话,我们不希望重启进程。但是,如果进程是通过Task Manager或其他方式被停掉的,我们会希望重启它。一个很好的例子是反病毒程序。我们想要确保恶意软件不能终止本应检测它的反病毒程序。
为了实现这一点,我们需要该Service为使用它的程序提供某种API,当该程序的用户选择“Quit”,程序会告知Service,程序的工作已经完成了,Service可以卸载自己了。
接下来,我们介绍一些构建基块,要理解本文的代码,它们是必备的。
为了获取我们的Service或其他可执行文件的路径,如下的函数是非常便利的。
/*** GetExePath() - returns the full path of the current executable.** @param values - none.* @return a std::wstring containing the full path of the current executable.*/std::wstring GetExePath(){wchar_t buffer[65536];GetModuleFileName(NULL, buffer, sizeof(buffer) / sizeof(*buffer));int pos = -1;int index = 0;while (buffer[index]){if (buffer[index] == L'\\' || buffer[index] == L'/'){pos = index;}index++;}buffer[pos + 1] = 0;return buffer;}
当开发Windows Service时(以及其他任何软件),拥有一个日志机制都是很重要的。我们有一个非常复杂的日志机制,但是就本文而言,我添加了一个最小的日志函数,名为WriteToLog。它的运行机制类似于printf,但是所有发送给它的内容不仅会被格式化,还会存储在一个日志文件中,以备日后检查。这个日志文件的大小会不断增长,因为会有新的日志条目追加到上面。
日志文件的路径,通常会位于Service的EXE的路径,但是,由于Service Isolation,在重启计算机后的一小段时间内,这个路径会变成 c:\Windows\System32,我们并不希望如此。所以,我们的日志函数会检查exe的路径,并且不会假设Current Directory在Service的生命周期内会保持不变。
/*** WriteToLog() - writes formatted text into a log file, and on screen (console)** @param values - formatted text, such as L"The result is %d",result.* @return - none*/void WriteToLog(LPCTSTR lpText, ...){FILE *fp;wchar_t log_file[MAX_PATH]{L""};if(wcscmp(log_file,L"") == NULL){wcscpy(log_file,GetExePath().c_str());wcscat(log_file,L"log.txt");}// find gmt time, and store in buf_timetime_t rawtime;struct tm* ptm;wchar_t buf_time[DATETIME_BUFFER_SIZE];time(&rawtime);ptm = gmtime(&rawtime);wcsftime(buf_time, sizeof(buf_time) / sizeof(*buf_time), L"%d.%m.%Y %H:%M", ptm);// store passed messsage (lpText) to buffer_inwchar_t buffer_in[BUFFER_SIZE];va_list ptr;va_start(ptr, lpText);vswprintf(buffer_in, BUFFER_SIZE, lpText, ptr);va_end(ptr);// store output message to buffer_out - enabled multiple parameters in swprintfwchar_t buffer_out[BUFFER_SIZE];swprintf(buffer_out, BUFFER_SIZE, L"%s %s\n", buf_time, buffer_in);_wfopen_s(&fp, log_file, L"a,ccs=UTF-8");if (fp){fwprintf(fp, L"%s\n", buffer_out);fclose(fp);}wcscat(buffer_out,L"\n");HANDLE stdOut = GetStdHandle(STD_OUTPUT_HANDLE);if (stdOut != NULL && stdOut != INVALID_HANDLE_VALUE){DWORD written = 0;WriteConsole(stdOut, buffer_out, wcslen(buffer_out), &written, NULL);}}
下面是一些我们用来存储看门狗可执行文件路径的函数,所以当计算机重启后,Service重新启动时,就能使用该路径。
BOOL CreateRegistryKey(HKEY hKeyParent, PWCHAR subkey){DWORD dwDisposition; //Verify new key is created or open existing keyHKEY hKey;DWORD Ret;Ret =RegCreateKeyEx(hKeyParent,subkey,0,NULL,REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS,NULL,&hKey,&dwDisposition);if (Ret != ERROR_SUCCESS){WriteToLog(L"Error opening or creating new key\n");return FALSE;}RegCloseKey(hKey); //close the keyreturn TRUE;}BOOL writeStringInRegistry(HKEY hKeyParent, PWCHAR subkey,PWCHAR valueName, PWCHAR strData){DWORD Ret;HKEY hKey;//Check if the registry existsRet = RegOpenKeyEx(hKeyParent,subkey,0,KEY_WRITE,&hKey);if (Ret == ERROR_SUCCESS){if (ERROR_SUCCESS !=RegSetValueEx(hKey,valueName,0,REG_SZ,(LPBYTE)(strData),((((DWORD)lstrlen(strData) + 1)) * 2))){RegCloseKey(hKey);return FALSE;}RegCloseKey(hKey);return TRUE;}return FALSE;}LONG GetStringRegKey(HKEY hKey, const std::wstring &strValueName,std::wstring &strValue, const std::wstring &strDefaultValue){strValue = strDefaultValue;TCHAR szBuffer[MAX_PATH];DWORD dwBufferSize = sizeof(szBuffer);ULONG nError;nError = RegQueryValueEx(hKey, strValueName.c_str(), 0, NULL,(LPBYTE)szBuffer, &dwBufferSize);if (nError == ERROR_SUCCESS){strValue = szBuffer;if (strValue.front() == _T('"') && strValue.back() == _T('"')){strValue.erase(0, 1); // erase the first characterstrValue.erase(strValue.size() - 1); // erase the last character}}return nError;}BOOL readStringFromRegistry(HKEY hKeyParent, PWCHAR subkey,PWCHAR valueName, std::wstring& readData){HKEY hKey;DWORD len = 1024;DWORD readDataLen = len;PWCHAR readBuffer = (PWCHAR)malloc(sizeof(PWCHAR) * len);if (readBuffer == NULL)return FALSE;//Check if the registry existsDWORD Ret = RegOpenKeyEx(hKeyParent,subkey,0,KEY_READ,&hKey);if (Ret == ERROR_SUCCESS){Ret = RegQueryValueEx(hKey,valueName,NULL,NULL,(BYTE*)readBuffer,&readDataLen);while (Ret == ERROR_MORE_DATA){// Get a buffer that is big enough.len += 1024;readBuffer = (PWCHAR)realloc(readBuffer, len);readDataLen = len;Ret = RegQueryValueEx(hKey,valueName,NULL,NULL,(BYTE*)readBuffer,&readDataLen);}if (Ret != ERROR_SUCCESS){RegCloseKey(hKey);return false;;}readData = readBuffer;RegCloseKey(hKey);return true;}else{return false;}}
本文中的程序有一项核心能力,那就是保护我们的SampleApp(我们将其称为宿主),当它未运行时,就重新启动它(所以叫做看门狗)。在真实场景中,我们会检查宿主是被用户终止的(这是允许的),还是被恶意软件终止的(这是不允许的),在后一种情况下,我们将会重启它(否则,如果用户选择Quit,但应用程序将继续“骚扰”系统并反复执行)。
如下是它如何实现的:
我们创建了一个Timer事件,每隔一定的时间(不应该过于频繁),我们会检查宿主的进程是否在运行,如果没有的话,我们就启动它。我们使用了一个静态布尔型标记(is_running),用来表明我们已经处于该代码块中了,所以在处理过程中时,能够避免再次调用。这是在WM_TIMER代码块中始终要做的事情,因为当定时器设置的频率过高的话,代码块在调用时,前一个WM_TIMER事件的代码依然在执行。
我们还通过检查g_bLoggedIn布尔标记来判断是否有用户登录。
case WM_TIMER:{if (is_running) break;WriteToLog(L"Timer event");is_running = true;HANDLE hProcessSnap;PROCESSENTRY32 pe32;bool found{ false };WriteToLog(L"Enumerating all processess...");// Take a snapshot of all processes in the system.hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);if (hProcessSnap == INVALID_HANDLE_VALUE){WriteToLog(L"Failed to call CreateToolhelp32Snapshot(). Error code %d",GetLastError());is_running = false;return 1;}// Set the size of the structure before using it.pe32.dwSize = sizeof(PROCESSENTRY32);// Retrieve information about the first process,// and exit if unsuccessfulif (!Process32First(hProcessSnap, &pe32)){WriteToLog(L"Failed to call Process32First(). Error code %d",GetLastError());CloseHandle(hProcessSnap); // clean the snapshot objectis_running=false;break;}// Now walk the snapshot of processes, and// display information about each process in turnDWORD svchost_parent_pid = 0;DWORD dllhost_parent_pid = 0;std::wstring szPath = L"";if (readStringFromRegistry(HKEY_LOCAL_MACHINE, (PWCHAR)SERVICE_REG_KEY, (PWCHAR)SERVICE_KEY_NAME, szPath)){m_szExeToFind = szPath.substr(szPath.find_last_of(L"/\\") + 1); // The process name is the executable name onlym_szExeToRun = szPath; // The executable to run is the full path}else{WriteToLog(L"Error reading ExeToFind from the Registry");}do{if (wcsstr( m_szExeToFind.c_str(), pe32.szExeFile)){WriteToLog(L"%s is running",m_szExeToFind.c_str());found = true;is_running=false;break;}if (!g_bLoggedIn){WriteToLog(L"WatchDog isn't starting '%s' because user isn't logged in",m_szExeToFind.c_str());return 1;}}while (Process32Next(hProcessSnap, &pe32));if (!found){WriteToLog(L"'%s' is not running. Need to start it",m_szExeToFind.c_str());if (!m_szExeToRun.empty()) // watchdog start the host app{if (!g_bLoggedIn){WriteToLog(L"WatchDog isn't starting '%s' because user isn't logged in",m_szExeToFind.c_str());return 1;}ImpersonateActiveUserAndRun();RunHost((LPWSTR)m_szExeToRun.c_str(), (LPWSTR)L"");}else{WriteToLog(L"m_szExeToRun is empty");}}CloseHandle(hProcessSnap);}is_running=false;break;
当我们想要测试这个解决方案时,我们雇佣了20个资深的和协作的测试人员。在整个工作过程中,越来多的测试均成功了。在某些时候,它在我们自己的Surface Pro笔记本电脑上运行地非常完美,但是,我们的一位员工报告说,在他的计算机上,在关闭之后,服务没有再次启动,或者在Ring 3下没有启动自身。这是一个好消息,因为在开发过程中,当你怀疑某个地方存在缺陷的时候,最糟糕的事情就是无法找到它,也无法重现它。总而言之,10%的测试者报告了问题。因此,这里发布的版本在我们员工的电脑上运行完美,然而2%的测试者仍然不时报告问题。换句话说,SampleApp在关闭计算机并打开后无法启动。
如下是对测试服务和看门狗的说明。
我们包含了一个由Visual Studio Wizard生成的样例应用,作为“宿主”应用,它会被看门狗确保一直运行。你可以单独运行它,外观如下面的图片所示。该应用没有做太多的事情。实际上,它一无是处……
/filters:no_upscale()/articles/windows-services-reliable-restart/en/resources/1figure-2-sample-app-1671630831799.jpg)
在后面的内容中,我们将提供测试服务和看门狗的指南。你可以在GitHub下载源码。
以管理员身份打开CMD。将当前目录变更至Service的EXE所在的路径并输入:
SG_RevealerService.exe Install#SampleApp.exe
/filters:no_upscale()/articles/windows-services-reliable-restart/en/resources/1figure-3-command-prompt-1671630831799.jpg)
你可以看到,我们有两个元素:
InstallService首先会启动SampleApp,从此之后,如果你尝试终止或杀死SampleApp的话,看门狗会在几秒钟后重启它。如果重启,关掉计算机并再次启动,你会发现Service会再次出现并启动SampleApp。这就是我们的Service的目标和功能。
最后,如果要停止和卸载服务,我们包含了一个uninstall.bat脚本,它如下所示:
sc stop sg_revealerservicesc delete sg_revealerservicetaskkill /f /im sampleapp.exetaskkill /f /im sg_revealerservice.exe
/filters:no_upscale()/articles/windows-services-reliable-restart/en/resources/1figure-4-delete-service-1671630831799.jpg)
Michael Haephrati是Secured Globe, Inc.的联合创始人和首席执行官,该公司于2008年与他的妻子Ruth Haephrati一起创建。Michael是一位音乐作曲家、发明家,也是一位专门从事软件开发和信息安全的专家。凭借30多年的经验,Michael形成了独特的视角,将技术和创新结合起来,并强调终端用户的体验。多年来,Michael领导了各种客户的创新项目和技术。他是“Learning C++”(https://www.manning.com/books/learning-c-plus-plus)的作者,该书由Manning Publications出版。
Ruth Haephrati是Secured Globe, Inc.的联合创始人和首席执行官,该公司于2008年与她的丈夫Michael Haephrati一起创建。Ruth是一位作家、演讲者、企业家、网络安全和网络取证专家。在过去的25年里,Ruth与微软和IBM等领先公司合作,担任顾问和C++实践专家。她最近参与了为一个国际客户开发的最先进的反恶意软件技术。在业余时间,Ruth是一位插画家、画家、野生动物摄影师和世界旅行者。
查看英文原文:The Service and the Beast: Building a Windows Service that Does Not Fail to Restart