写点什么

构建重启后依然可用的 Windows 服务

作者:Michael Haephrati, Ruth Haephrati

  • 2023-03-09
    北京
  • 本文字数:15192 字

    阅读完需:约 50 分钟

构建重启后依然可用的Windows服务

当使用 C++为 Windows 编程时,使用 Windows 服务(Windows Services)几乎是难以避免的。在微软 Windows 操作系统中,Windows 服务发挥着重要的作用,它们能够创建和管理长时间运行的进程,这些进程能够在睡眠、休眠、重启和关机的过程中幸存下来。但是,如果无法做到这一点会怎样呢?在选中快速启动(Fast Startup)时,关闭计算机会导致服务无法重启,这会给程序带来灾难性的后果。微软在 Windows Vista 中引入的Service Isolation可能会导致这类灾难性的后果,在本文中将会阐述如何解决它。

感谢 Windows 服务


多年以来,我们一直在使用 Windows 服务,但是不管我们觉得有多么了解服务,或者有多么自信能够处理它,却始终会遇到更多的问题、挑战和麻烦。其中有些问题根本是没有文档的,或者我们“幸运”一点的话,会有一点糟糕的文档。


自从微软引入服务隔离之后,我们遇到的最令人恼火的问题之一就是当快速启动选中时,计算机关闭后,无法重启服务。鉴于我们没有找到现成的解决方案,所以我们决定自动动手实现一个,这促成了持久化服务的开发。


但是,在深入研究和解释我们的解决方案之前,我们首先从基础知识开始,解释什么是服务,以及为什么要使用 Windows 服务。


NT 服务(也叫做 Windows 服务)指的是由 NT 内核的服务控制管理器(Service Control Manager)加载的特殊进程,它会在 Windows 启动(在用户登录前)立即在后台运行。我们使用服务来执行核心和底层的操作系统任务,比如 Web 服务、事件日志、文件服务、帮助和支持、打印、加密和错误报告。


此外,服务使我们能够创建可执行的、长时间运行的应用程序。原因在于服务会在自己的 Windows 会话环境中运行,所以它不会干扰应用程序的其他组件或会话。显然,我们期望服务会在计算机启动后也自动启动,我们马上就会讨论该问题。


进一步来讲,这里显然有一个问题:我们为什么需要持久化的服务?答案很明显,服务应该能够:


  • 持续在后台运行。

  • 在已登录用户的会话中,调用自身。

  • 作为一个看门狗(watchdog),确保给定的应用程序一直在运行。


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用户下,另一个在当前登录用户下。



/*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 error NULL, // No string to be // formatted needed dwLastError, // Hey Windows: Please // explain this error! MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Do it in the standard // language lpBuffer, // Put the message here 255, // Number of bytes to store the message NULL); } 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); else WriteToLog(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, PVOID       pContext, 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 one DWORD 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 process WCHAR* 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 memory if (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 parameters ZeroMemory(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 it BOOL 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 name WTSFreeMemory(UserName); } } WTSFreeMemory(SessionInfo); } return user;}
复制代码


在服务启动后不久,我们就要使用该函数。只要没有用户登录,这个函数就会返回一个空字符串,如果这样的话,我们就知道应该继续等待。

看门狗是 Service 的好朋友


Service 与看门狗机制协同使用是很理想的方案。


这种机制将确保一个给定应用始终处于运行状态,如果它异常关闭的话,看门狗会重新启动它。我们要始终记住,如果用户通过Quit退出的话,我们不希望重启进程。但是,如果进程是通过Task Manager或其他方式被停掉的,我们会希望重启它。一个很好的例子是反病毒程序。我们想要确保恶意软件不能终止本应检测它的反病毒程序。


为了实现这一点,我们需要该 Service 为使用它的程序提供某种 API,当该程序的用户选择“Quit”,程序会告知 Service,程序的工作已经完成了,Service 可以卸载自己了。

一些构建基块


接下来,我们介绍一些构建基块,要理解本文的代码,它们是必备的。

GetExePath


为了获取我们的 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;}
复制代码

WriteLogFile


当开发 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_time    time_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_in wchar_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 swprintf wchar_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 key    HKEY  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 key    return TRUE;}
BOOL writeStringInRegistry(HKEY hKeyParent, PWCHAR subkey, PWCHAR valueName, PWCHAR strData){ DWORD Ret; HKEY hKey; //Check if the registry exists Ret = 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 character strValue.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 exists DWORD 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; }}
复制代码

检查宿主(Host)是否在运行


本文中的程序有一项核心能力,那就是保护我们的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 unsuccessful if (!Process32First(hProcessSnap, &pe32)) { WriteToLog(L"Failed to call Process32First(). Error code %d",GetLastError()); CloseHandle(hProcessSnap); // clean the snapshot object is_running=false; break; }
// Now walk the snapshot of processes, and // display information about each process in turn DWORD 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 only m_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;
复制代码

如何测试 Service


当我们想要测试这个解决方案时,我们雇佣了 20 个资深的和协作的测试人员。在整个工作过程中,越来多的测试均成功了。在某些时候,它在我们自己的 Surface Pro 笔记本电脑上运行地非常完美,但是,我们的一位员工报告说,在他的计算机上,在关闭之后,服务没有再次启动,或者在Ring 3下没有启动自身。这是一个好消息,因为在开发过程中,当你怀疑某个地方存在缺陷的时候,最糟糕的事情就是无法找到它,也无法重现它。总而言之,10%的测试者报告了问题。因此,这里发布的版本在我们员工的电脑上运行完美,然而 2%的测试者仍然不时报告问题。换句话说,SampleApp在关闭计算机并打开后无法启动。


如下是对测试服务和看门狗的说明。

SampleApp


我们包含了一个由 Visual Studio Wizard 生成的样例应用,作为“宿主”应用,它会被看门狗确保一直运行。你可以单独运行它,外观如下面的图片所示。该应用没有做太多的事情。实际上,它一无是处……



在后面的内容中,我们将提供测试服务和看门狗的指南。你可以在GitHub下载源码。

从 CMD 中运行

以管理员身份打开 CMD。将当前目录变更至 Service 的 EXE 所在的路径并输入:


SG_RevealerService.exe Install#SampleApp.exe



你可以看到,我们有两个元素:


  • command 元素,这里是Install

  • argument 元素,通过哈希分隔符(#)连接至命令元素,应该是我们希望看门狗观察的可执行文件。


Service 首先会启动 SampleApp,从此之后,如果你尝试终止或杀死SampleApp的话,看门狗会在几秒钟后重启它。如果重启,关掉计算机并再次启动,你会发现 Service 会再次出现并启动SampleApp。这就是我们的 Service 的目标和功能。

卸载


最后,如果要停止和卸载服务,我们包含了一个uninstall.bat脚本,它如下所示:


sc stop sg_revealerservicesc delete sg_revealerservicetaskkill /f /im sampleapp.exetaskkill /f /im sg_revealerservice.exe
复制代码


结论


  • Windows Service 在微软 Windows 操作系统中起着关键作用,它支持创建和管理长期运行的进程。

  • 在有些场景下,如果勾选了“快速启动”,在正常关闭并重启计算机后,服务往往无法重启。

  • 本文的目的是创建一个持久化的服务,在 Windows 重新启动或关机后,能够始终运行并重新启动。

  • 其中一个主要的问题与 Service Isolation 有关。隔离本身(在 Windows Vista 版本中引入)是很重要和强大的,然而,当我们需要与用户空间交互时,这会产生一些限制。

  • 当服务重新启动时,我们希望它能与用户空间进行交互,然而它不能发生地太早(在任何用户登录之前)。不过,你可以通过订阅登录事件来解决这个问题。

  • Service 与看门狗机制协同使用是很理想的方案。这种机制能够确保给定的应用一直在运行,并且在异常关闭时,它将重新启动。我们在前面描述的方法的基础上,成功地开发了这个机制,这使得它可以一直运行,在用户登录时得到提醒,并且能够与用户空间进行交互。

  • 定时器事件能够用来监控被观察进程的运行。

  • 在开发过程中,好的日志机制始终是非常有用的,我们可以使用简单的日志工具,并在需要的时候,使用更为复杂的工具。

  • 最终的解决方案必须要进行测试。代码被确认并验证可以运行后,多达 2%的测试人员依然可能会报告错误,这是有一定原因的。


作者简介:

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


相关阅读:

Windows 11发布重大更新:ChatGPT版Bing集成到任务栏中,可快速访问AI聊天功能

Kubernetes 1.26 版本正式发布:改进 Windows 支持,加强网络安全和管理功能

2023-03-09 08:006020

评论 1 条评论

发布
用户头像
详细!
2023-03-09 09:41 · 陕西
回复
没有更多了
发现更多内容
构建重启后依然可用的Windows服务_语言 & 开发_InfoQ精选文章