正如WTSQueryUserToken 引发错误 1008 中所讨论的,即使在 LocalSystem 下运行时,我也无法让我的 Windows 服务在特定用户登录后立即在其桌面上启动交互式进程。
建议的解决方案是处理SERVICE_CONTROL_SESSIONCHANGE控制代码并使用传递的dwSessionId. 这是所有代码(抱歉,它很长,但我被告知无论如何都要在这里发布):
// These headers just contain system header #include's function prototypes
// and global variable declarations. If a variable below seems like it is
// undefined, rest assured that it *is* defined in one of these headers.
#include "events.h"
#include "main.h"
int __cdecl _tmain(int argc, LPTSTR argv[]) {
sysStart = system_clock::now();
LogInit();
// If command-line parameter is "install", install the service.
// Otherwise, the service is probably being started by the SCM
if (lstrcmpi(argv[1], L"install") == 0) {
return SvcInstall();
}
SERVICE_TABLE_ENTRY dispatchTable[] = {
{ &svcName[0], (LPSERVICE_MAIN_FUNCTION)SvcMain },
{ nullptr, nullptr }
};
// This call returns when the service has stopped. The
// process should simply terminate when the call returns
if (!StartServiceCtrlDispatcher(dispatchTable)) {
ReportSvcEvent("StartServiceCtrlDispatcher");
}
return ERROR_SUCCESS;
}
char* WINAPI GetTimestamp(string& buf) {
int ms = (high_resolution_clock::now().
time_since_epoch().count() / 1000000) % 1000;
auto tt = system_clock::to_time_t(
system_clock::now());
tm time;
localtime_s(&time, &tt);
strftime(&buf[0], 21, "[%d-%m-%Y %T", &time);
snprintf(&buf[0], 26, "%s.%03d] ", &buf[0], ms);
buf[25] = ' ';
return &buf[0];
}
bool WINAPI LaunchDebugger(void) {
// Get System directory, typically C:\Windows\System32
wstring systemDir(MAX_PATH + 1, '\0');
UINT nChars = GetSystemDirectory(&systemDir[0], systemDir.length());
if (nChars == 0) {
return false; // failed to get system directory
}
systemDir.resize(nChars);
// Get process ID and create the command line
// wostringstream ss;
// ss << systemDir << L"\\vsjitdebugger.exe -p " << GetCurrentProcessId();
wstring cmdLine = L"";
// Start debugger process
STARTUPINFOW si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcess(nullptr, &cmdLine[0], nullptr,
nullptr, false, 0, nullptr, nullptr, &si, &pi)) {
return false;
}
// Close debugger process handles to eliminate resource leaks
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
// Wait for the debugger to attach
while (!IsDebuggerPresent()) {
Sleep(100);
}
// Stop execution so the debugger can take over
DebugBreak();
return true;
}
VOID WINAPI LogActiveTime(void) {
// The computer is shutting down - write an entry to logFile to reflect
// this, prefixed with a null byte to mark the current file position
// (used for parsing in the timestamp on the next system boot)
logFile << '\0';
LogMessage("User action", "System shutting down after being "
"active for " + DurationString(system_clock::now() - sysStart));
logFile.close();
// If the log file contains > 40 lines (10 boot/shutdown cycles),
// remove the first 4 lines (the earliest boot/shutdown cycle).
// This stops the file from getting too long to read easily
ifstream inFile(logFilePath);
string line;
auto count = 0;
while (getline(inFile, line)) {
count++;
}
}
DWORD WINAPI LogError(const string& funcName) {
auto err = 0;
LogMessage(funcName, system_category(
).message(err = GetLastError()), true);
return err;
}
DWORD WINAPI LogInactiveTime(void) {
// Create a new log file to be used as the input on the next run
LogInit("temp");
// Open the existing log file for reading and find the last shutdown
// log entry by copying its contents to the new file until a null byte
// or EOF is found (see LogActiveTime() for more info)
ifstream inFile(logFilePath);
if (!inFile) {
return LogError("LogInactiveTime");
}
char ch = inFile.get();
while (ch != '\0' && !inFile.eof()) {
logFile << ch;
ch = inFile.get();
}
if (inFile.eof()) {
// No shutdown log entry was found, i.e. this is probably the first
// time the service has run on the current instance of our log file.
// Close the temp file and re-open the original log file before
// returning, otherwise future messages won't make it to the file!
LogInit();
return ERROR_SUCCESS;
}
// At this point we can be sure that a valid shutdown log entry
// exists, so we now need to parse it into a chrono::time_point.
// Also save the entry's starting position in pos for later use
auto pos = inFile.tellg();
auto tt = system_clock::to_time_t(sysStart);
tm start, end = { 0 };
localtime_s(&start, &tt);
inFile >> get_time(&end, "[%d-%m-%Y %T");
if (inFile.fail() || inFile.bad()) {
return LogError("LogInactiveTime");
}
// Ensure that both time_points refer to
// the correct time, regardless of DST
end.tm_isdst = start.tm_isdst;
sysEnd = system_clock::from_time_t(mktime(&end));
// Go back to the *actual* start of the shutdown
// log entry so we can copy it into the new file
inFile.seekg(pos);
// Finish copying over the rest of our existing log
// file, then close it and replace it with the new one
ch = inFile.get();
while (!inFile.eof()) {
logFile << ch;
ch = inFile.get();
}
inFile.close();
remove(logFilePath.c_str());
logFile.close();
rename("temp", logFilePath.c_str());
// Finally, do what we *actually* came here to do!
LogMessage("User action", "System booting after being "
"inactive for " + DurationString(sysStart - sysEnd));
return ERROR_SUCCESS;
}
VOID WINAPI LogInit(const string& filePath) {
setlocale(LC_ALL, "en_US.UTF8");
if (logFile.is_open()) {
logFile.close();
}
logFile.open(filePath == "" ?
logFilePath : filePath, ios::app);
if (!logFile) {
exit(GetLastError());
}
}
VOID WINAPI LogMessage(const string& funcName,
const string& msg, bool isError) {
if (!logFile.is_open()) {
LogInit();
}
string buf(52, '\0');
snprintf(&buf[0], 52, "%s%-6s %-18s ", GetTimestamp(buf),
isError ? "ERROR:" : "INFO:", &(funcName + ':')[0]);
buf[51] = ' ';
logFile << buf << msg << endl;
}
VOID WINAPI ReportSvcEvent(const string& funcName) {
HANDLE eventSrc = RegisterEventSource(nullptr, &svcName[0]);
if (eventSrc != nullptr) {
LPCSTR errParams[2] = { "WinUtilities" };
char buf[MAX_PATH];
StringCchPrintfA(buf, MAX_PATH, "Function '%s' failed: %s",
funcName.c_str(), system_category().message(GetLastError(
)).c_str());
errParams[1] = buf;
ReportEventA(eventSrc, // event log handle
EVENTLOG_ERROR_TYPE, // event type
0, // event category
SVC_ERROR, // event identifier
nullptr, // no security identifier
2, // size of lpszStrings array
0, // no binary data
errParams, // array of strings
nullptr); // no binary data
DeregisterEventSource(eventSrc);
}
}
VOID WINAPI ReportSvcStatus(DWORD newState,
DWORD exitCode, DWORD waitHint) {
static DWORD dwCheckPoint = 1;
static unordered_map<int, string> svcStates;
if (svcStates.empty()) {
// Initialise mapping from service state codes to readable strings
svcStates.insert({ SERVICE_STOPPED, "Stopped" });
svcStates.insert({ SERVICE_START_PENDING, "Start Pending" });
svcStates.insert({ SERVICE_STOP_PENDING, "Stop Pending" });
svcStates.insert({ SERVICE_RUNNING, "Running" });
svcStates.insert({ SERVICE_CONTINUE_PENDING, "Continue Pending" });
svcStates.insert({ SERVICE_PAUSE_PENDING, "Pause Pending" });
svcStates.insert({ SERVICE_PAUSED, "Paused" });
}
// Update the SERVICE_STATUS structure with the new passed-in values
svcStatus.dwCurrentState = newState;
svcStatus.dwWin32ExitCode = exitCode;
svcStatus.dwWaitHint = waitHint;
if (newState == SERVICE_START_PENDING) {
svcStatus.dwControlsAccepted = 0;
} else {
svcStatus.dwControlsAccepted =
SERVICE_ACCEPT_SESSIONCHANGE |
SERVICE_ACCEPT_STOP |
SERVICE_ACCEPT_PRESHUTDOWN;
}
if (newState == SERVICE_RUNNING ||
newState == SERVICE_STOPPED) {
svcStatus.dwCheckPoint = 0;
} else {
svcStatus.dwCheckPoint = dwCheckPoint++;
}
// Report the status of the service to the SCM and our log file
if (!SetServiceStatus(statusHandle, &svcStatus)) {
LogError("SetServiceStatus");
} else {
LogMessage("SetServiceStatus", "Service status " \
"updated to '" + svcStates[newState] + "'.");
}
}
DWORD WINAPI SvcCtrlHandler(DWORD ctrlCode, DWORD
eventType, LPVOID eventData, LPVOID context) {
switch (ctrlCode) {
case SERVICE_CONTROL_SESSIONCHANGE: {
auto sessionId = ((WTSSESSION_NOTIFICATION*
)eventData)->dwSessionId;
switch (eventType) {
case WTS_SESSION_LOGON: {
string userName;
DWORD size;
WTSQuerySessionInformationA(WTS_CURRENT_SERVER_HANDLE, sessionId,
WTS_INFO_CLASS::WTSUserName, (LPSTR*)&userName[0], &size);
ReportSvcEvent("log on");
// A user has successfully logged on to the PC. Now we can start
// an interactive worker process under that user's account which
// will perform the actual work that we want to do
STARTUPINFO si = { 0 };
si.cb = sizeof(si);
si.wShowWindow = true;
HANDLE hToken;
if (!WTSQueryUserToken(sessionId, &hToken)) {
LogError("WTSQueryUserToken");
return ERROR_CALL_NOT_IMPLEMENTED;
}
wstring cmdLine = L"C:\\Path\\to\\my\\app.exe";
if (!CreateProcessAsUser(hToken, &cmdLine[0], nullptr, nullptr, nullptr,
false, CREATE_NO_WINDOW, nullptr, nullptr, &si, &workerProc)) {
LogError("CreateProcessAsUser");
return ERROR_CALL_NOT_IMPLEMENTED;
}
CloseHandle(hToken);
break;
} default: {
break;
}
}
break;
} case SERVICE_CONTROL_STOP: {
// Signal the service to stop
ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
SetEvent(svcStopEvent);
break;
} case SERVICE_CONTROL_PRESHUTDOWN: {
LogActiveTime();
break;
} default: {
return ERROR_CALL_NOT_IMPLEMENTED;
}
}
return NO_ERROR;
}
VOID WINAPI SvcInit(DWORD argc, LPTSTR argv[]) {
// Get the time at which the last shutdown occurred, and
// log the duration for which the system was inactive
if (LogInactiveTime() > 0) {
return;
}
// Create an event. The control handler function (SvcCtrlHandler)
// signals this event when it receives the stop control code
svcStopEvent = CreateEvent(
nullptr, // default security attributes
TRUE, // manual reset event
FALSE, // not signaled
nullptr); // no name
if (svcStopEvent == nullptr) {
LogError("CreateEvent");
ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
return;
}
// Report running status when initialisation is complete
ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
// Wait until our stop event has been signalled
WaitForSingleObject(svcStopEvent, INFINITE);
// Code execution won't reach here until the service has been
// fully stopped. Report this to the SCM when it happens, then
// terminate the worker process and clean up its handles
ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
if (workerProc.hProcess) {
TerminateProcess(workerProc.hProcess, 0);
CloseHandle(workerProc.hProcess);
CloseHandle(workerProc.hThread);
}
}
DWORD WINAPI SvcInstall(void) {
TCHAR path[MAX_PATH];
if (!GetModuleFileName(nullptr, path, MAX_PATH)) {
return LogError("GetModuleFileName");
}
// Get a handle to the SCM database
auto scm = OpenSCManager(
nullptr, // local computer
nullptr, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if (scm == nullptr) {
return LogError("OpenSCManager");
}
// Create the service
auto svc = CreateService(
scm, // SCM database
&svcName[0], // name of service
L"Windows Utilities", // service name to display
SERVICE_ALL_ACCESS, // desired access
SERVICE_WIN32_OWN_PROCESS, // service type
SERVICE_AUTO_START, // start type
SERVICE_ERROR_NORMAL, // error control type
path, // path to service's binary
nullptr, // no load ordering group
nullptr, // no tag identifier
nullptr, // no dependencies
nullptr, // LocalSystem account
nullptr); // no password
if (svc == nullptr) {
CloseServiceHandle(scm);
return LogError("CreateService");
}
SERVICE_DESCRIPTION sd;
sd.lpDescription = const_cast<LPTSTR>(L"Logs system "
"shutdown events to a text file on the desktop. "
"Also creates a system-wide hot key to perform "
"internet searches on any selected text.");
if (!ChangeServiceConfig2(
svc, // handle to service
SERVICE_CONFIG_DESCRIPTION, // change: description
&sd)) // new description
{
CloseServiceHandle(svc);
CloseServiceHandle(scm);
return LogError("ChangeServiceConfig2");
}
CloseServiceHandle(svc);
CloseServiceHandle(scm);
LogMessage("SvcInstall", "Service installed successfully.");
return ERROR_SUCCESS;
}
VOID WINAPI SvcMain(DWORD argc, LPTSTR argv[]) {
// Register the handler function for the service
statusHandle = RegisterServiceCtrlHandlerEx(
&svcName[0], SvcCtrlHandler, 0);
if (!statusHandle) {
ReportSvcEvent("RegisterServiceCtrlHandlerEx");
return;
}
// These SERVICE_STATUS members remain as set here
svcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
svcStatus.dwServiceSpecificExitCode = 0;
// Report initial status to the SCM
ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
// Perform service-specific initialization and work
SvcInit(argc, argv);
}
不起作用的部分在SvcCtrlHandler()函数中,我试图在其中捕获上述控制代码。
我什至已经用 C# 重写了整个事情(这是我应该首先使用的语言,因为我的代码现在变得更加干净和清晰了),你猜怎么着?我的方法仍然存在完全相同的问题OnSessionChange()!
当我冷启动计算机并允许我的 PC 自动登录到我的单个用户帐户时,没有任何反应(即没有app.exe启动)。但是,如果我然后注销并重新登录,我会得到我正在寻找的结果。
所以似乎我的服务是最后几个加载的服务之一,这阻止了它正确捕获SERVICE_CONTROL_SESSIONCHANGE控制代码。我怎样才能解决这个问题?大都会!:D