最近遇到一个小问题,我需要在一个服务进程中去启动一个用户GUI进程。按常理来说这很简单,通常情况下调用ShellExecute这个API即可。这种方法在XP的年代似乎是完美的,但由于现在大多已经到了Win7,这个方法似乎已经不好用了。追溯原因要谈到微软给XP以后的操作系统添加了Session隔离机制。网络上有很多介绍Windows Session机制的资料,这里就不再多余讨论,反正这个机制的出现让很多人为之头疼!
当我遇到这个问题时,我第一时间在网上搜索了资料,资料很多但也很乱,其中大部分资料都是乱拷贝粘贴的。里面充斥了很多错误,而这些发布者似乎从来没有去验证过他所拷贝的文章、代码是否正确!
出于以上的原因,在我解决这个问题后,我思考着将我的代码用一个简单的示例展示出来,用以帮助以后遇到该问题的朋友。我本着尽可能严谨的态度来书写这篇文章,但尽管这样,可能还是有所疏漏,如果您发现了,请告诉我,谢谢!
本文我将分两个部分介绍。首先,我们先创建一个足够简单的服务程序。接着,在此服务程序的基础上,实现用它来启动一个用户GUI程序。温馨提示,在本文我不会讲到什么是Session隔离,因为能力有限,我想我无法讲解清楚这个问题,但我会在本文末尾提供一些相关的链接供读者参考。另外,我会简单提及一下如何构建一个基本的Win32服务程序,以帮助没有任何服务程序开发经验的读者能更好的阅读,谢谢。
1.创建一个简单的服务进程
首先,我们搭建一个简单Win32服务进程框架,它的工作足够简单:在服务启动后,向指定文件输出一行”Hello Win32 Service”即可!
一个服务程序其实有它较为固定的框架,通常它包含一个main函数和两个回调函数。显而易见main函数还是唯一入口,另外两个回调函数会被自动调用而无需我们关心。因此,一个服务程序大概看起来就是这样的:
void WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
void WINAPI Handler(DWORD fdwControl);
int main(int argc, char const *argv[])
{
return 0;
}
让我们先从main函数开始。先定义几个全局变量,因为即将用到它们:
//全局数据
TCHAR ServiceName[] = _TEXT("ServiceDemo"); //服务名称
SERVICE_STATUS ServiceStatus; //服务状态
SERVICE_STATUS_HANDLE hStatus; //服务状态句柄
接着改写main函数:
int main(int argc, char const *argv[])
{
SERVICE_TABLE_ENTRY ServiceTable[] = {
{ServiceName, ServiceMain},
{NULL, NULL},
};
return StartServiceCtrlDispatcher(ServiceTable);
}
这里我们定义了一个SERVICE_TABLE_ENTRY类型的数组,通常称它为分派表。一个服务进程可以包含若干个子服务,每一个服务都必须在专门的分派表中注册。这个表的每一项都是一个 SERVICE_TABLE_ENTRY 结构对象。它有两个域:
lpServiceName: 指向表示服务名称字符串的指针;当定义了多个服务时,这个域必须指定;
lpServiceProc: 指向服务主函数的指针(服务入口点);
这里,我们传入了ServiceName和ServiceMain作为参数构造了第一个SERVICE_TABLE_ENTRY对象。无需构造太多,因为该服务进程只需一个服务就足够了。值得注意的是,分派表的最后一项必须是服务名和服务主函数的 NULL 指针,用以表示该数组的结束。
紧接着,调用StartServiceCtrlDispatcher方法,并传入了ServiceTable分派表作为其参数。服务控制管理器(SCM:Services Control Manager)是一个管理系统所有服务的进程。当 SCM 启动某个服务时,它等待该服务进程的主线程来调用 StartServiceCtrlDispatcher 函数,并将分派表传递给 StartServiceCtrlDispatcher。这一调用将把调用进程的主线程转换为控制分派器。该分派器启动一个新线程,该线程运行分派表中每个服务的 ServiceMain 函数。另外分派器还监视程序中所有服务的执行情况,然后分派器将控制请求从 SCM 传给各个服务,至于各个服务将如何响应这些控制请求,稍后再来关注。
另一个值得注意的是,如果 StartServiceCtrlDispatcher 函数30秒没有被调用,便会报错,为了避免这种情况,我们必须在 ServiceMain 函数中或在非主函数的单独线程中初始化服务分派表。而在本文所描述的服务中不需要防范这样的情况。
分派表中所有的服务执行完之后(例如,用户通过“服务”控制面板程序停止它们),或者发生错误时,StartServiceCtrlDispatcher 调用返回,然后服务主进程终止。
ServiceMain函数:
void WINAPI ServiceMain(DWORD argc, LPTSTR* argv)
{
ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS;
ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
ServiceStatus.dwWin32ExitCode = NO_ERROR;
ServiceStatus.dwServiceSpecificExitCode = NO_ERROR;
ServiceStatus.dwCheckPoint = 0;
ServiceStatus.dwWaitHint = 0;
hStatus = RegisterServiceCtrlHandler(ServiceName, Handler);
if(!hStatus)
{
DWORD dwError = GetLastError();
WriteErrorToLog(dwError, "RegisterServiceCtrlHandler");
return ;
}
InitService();
//设置服务状态
ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hStatus, &ServiceStatus);
Run();
}
该函数是一个服务的入口点。ServiceMain 应该尽可能早的为服务注册控制处理器。这要通过调用 RegisterServiceCtrlHadler 函数来实现。你要将两个参数传递给此函数:服务名和指向 ControlHandlerfunction 的指针。控制处理器函数,它指示控制分派器调用 Handler 函数处理 SCM 控制请求。注册完控制处理器之后,获得状态句柄(hStatus)。通过调用 SetServiceStatus 函数,用 hStatus 向 SCM 报告服务的状态。
另外,在ServiceMain 函数中,还对ServiceStatus结构体的成员设置了一些值,关于这些值的含义可以查看MSDN给出的文档,这里有详细的介绍。
接着,调用RegisterServiceCtrlHandler函数给该服务注册了一个控制信号处理函数Handler。然后使用InitService函数来做一些初始化工作。完成后,设置服务的状态为正在运行中,并且调用Run函数开始真正的“服务”。
Handler函数:
这是最后一个要说明的函数了,它看上去是这样的:
void WINAPI Handler(DWORD fdwControl)
{
switch(fdwControl)
{
case SERVICE_CONTROL_STOP:
{
WriteMsgToLog("ServiceDemo 服务停止...");
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = 0;
SetServiceStatus(hStatus, &ServiceStatus);
}
break;
case SERVICE_CONTROL_SHUTDOWN:
{
WriteMsgToLog("ServiceDemo 服务终止...");
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = 0;
SetServiceStatus(hStatus, &ServiceStatus);
}
break;
default:
break;
}
}
这里处理了SERVICE_CONTROL_STOP和SERVICE_CONTROL_SHUTDOWN消息,因为在ServiceMain函数的开头我们注册了这两个消息,表明我们希望自己处理这些消息。
到了这里,一个基本的服务程序就简单介绍完毕,稍后将给出整个服务程序的代码,你可以编译这份代码,并使用下面两条命令去安装和删除这个服务进程,安装成功后,可以到服务管理器中去启动该服务。
#安装服务 注意“binpath=”后有一个空格
sc create ServiceDemo binpath= C:/ServiceDemo.exe
#删除服务 删除前需先停止服务
sc delete ServiceDemo
如果不出什么意外,你能在代码定义的日志文件中看到一些输出。
完整代码:
#include
#include
#include
#define LOGFILE "C:\\Msg.log"
void WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
void WINAPI Handler(DWORD fdwControl);
//日志记录的相关方法
void WriteMsgToLog(const char *Msg);
void WriteErrorToLog(DWORD dwErrorCode, const char *MsgInfo);
//服务相关方法
void InitService();
void Run();
//全局数据
TCHAR ServiceName[] = _TEXT("ServiceDemo"); //服务名称
SERVICE_STATUS ServiceStatus; //服务状态
SERVICE_STATUS_HANDLE hStatus; //服务状态句柄
int main(int argc, char const *argv[])
{
SERVICE_TABLE_ENTRY ServiceTable[] = {
{ ServiceName, ServiceMain },
{ NULL, NULL },
};
return StartServiceCtrlDispatcher(ServiceTable);
}
void WINAPI ServiceMain(DWORD argc, LPTSTR* argv)
{
ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS;
ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
ServiceStatus.dwWin32ExitCode = NO_ERROR;
ServiceStatus.dwServiceSpecificExitCode = NO_ERROR;
ServiceStatus.dwCheckPoint = 0;
ServiceStatus.dwWaitHint = 0;
hStatus = RegisterServiceCtrlHandler(ServiceName, Handler);
if (!hStatus)
{
DWORD dwError = GetLastError();
WriteErrorToLog(dwError, "RegisterServiceCtrlHandler");
return;
}
InitService();
//设置服务状态
ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hStatus, &ServiceStatus);
Run();
}
void WINAPI Handler(DWORD fdwControl)
{
switch (fdwControl)
{
case SERVICE_CONTROL_STOP:
{
WriteMsgToLog("ServiceDemo 服务停止...");
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = 0;
SetServiceStatus(hStatus, &ServiceStatus);
}
break;
case SERVICE_CONTROL_SHUTDOWN:
{
WriteMsgToLog("ServiceDemo 服务终止...");
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = 0;
SetServiceStatus(hStatus, &ServiceStatus);
}
break;
default:
break;
}
}
void InitService()
{
WriteMsgToLog("ServiceDemo 服务启动...");
}
void Run()
{
WriteMsgToLog("Hello Win32 Service");
}
void WriteMsgToLog(const char *Msg)
{
FILE *file = NULL;
fopen_s(&file, LOGFILE, "a+");
if (!file || !Msg)
{
return;
}
fprintf_s(file, "%s\n", Msg);
fflush(file);
fclose(file);
file = NULL;
}
void WriteErrorToLog(DWORD dwErrorCode, const char *MsgInfo)
{
if (!MsgInfo) return;
char Msg[128] = { 0 };
sprintf_s(Msg, "%s Faild: %lu\n", MsgInfo, dwErrorCode);
WriteMsgToLog(Msg);
}
2.创建用户GUI进程
终于可以开始本文的重点了。首先,我们先试着启动一个notepad.exe,因此请在上面的源码中添加如下的定义:
TCHAR szApp[MAX_PATH] = _TEXT("notepad.exe")
接着,创建一个函数用以创建GUI进程。
void CreateMyProcess()
{
DWORD dwSessionID = WTSGetActiveConsoleSessionId();
HANDLE hToken = NULL;
HANDLE hTokenDup = NULL;
LPVOID pEnv = NULL;
STARTUPINFO si;
PROCESS_INFORMATION pi;
//获取当前处于活动状态用户的Token
if (!WTSQueryUserToken(dwSessionID, &hToken))
{
DWORD nCode = GetLastError();
WriteErrorToLog(nCode, "WriteErrorToLog");
CloseHandle(hToken);
return;
}
//复制新的Token
if (!DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hTokenDup))
{
DWORD nCode = GetLastError();
WriteErrorToLog(nCode, "DuplicateTokenEx");
CloseHandle(hToken);
return;
}
//创建环境信息
if (!CreateEnvironmentBlock(&pEnv, hTokenDup, FALSE))
{
DWORD nCode = GetLastError();
WriteErrorToLog(nCode, "CreateEnvironmentBlock");
CloseHandle(hTokenDup);
CloseHandle(hToken);
return;
}
//设置启动参数
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
si.lpDesktop = _TEXT("winsta0\\default");
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
//开始创建进程
DWORD dwCreateFlag = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT;
if (!CreateProcessAsUser(hTokenDup, szApp, NULL, NULL, NULL, FALSE, dwCreateFlag, pEnv, NULL, &si, &pi))
{
DWORD nCode = GetLastError();
WriteErrorToLog(nCode, "CreateProcessAsUser");
DestroyEnvironmentBlock(pEnv);
CloseHandle(hTokenDup);
CloseHandle(hToken);
return;
}
//附加操作,回收资源
//等待启动的进程结束
WaitForSingleObject(pi.hProcess, INFINITE);
WriteMsgToLog("子进程结束,回收资源...");
DestroyEnvironmentBlock(pEnv);
CloseHandle(hTokenDup);
CloseHandle(hToken);
}
看看CreateMyProcess()函数的部分细节。首先,我们调用WTSGetActiveConsoleSessionId() API尝试获取当前处于活动状态的用户的Session ID。为什么要这么做?因为我们需要以当前用户的权限去创建这个GUI进程,而要想这么做就得取到当前活动用户的权限令牌(Token),要取得权限令牌,首先就得找到是哪一个用户。
接着就可以使用WTSQueryUserToken() API来取得该用户的令牌了,我们将它存放到了hToken句柄中。紧接着用DuplicateTokenEx() API复制一个已经存在的令牌来产生一个新的令牌。值得注意的是,因为新的令牌将用于创建进程,即用于CreateProcessAsUser() API的参数,所以按照MSDN上面的说法,在复制令牌时,需要传入一些指定的参数,具体可以参看这里。
然后,使用CreateEnvironmentBlock() API获取了指定用户的环境变量,这个环境变量会作为参数传递给CreateProcessAsUser() API。最后,简单的对STARTUPINFO和PROCESS_INFORMATION结构体的一些成员赋予了一些特定的值,就开始调用CreateProcessAsUser() API,这个API完成了主要的工作。但它的一些参数需要特别注意,不过这些MSDN上都有详细说明!
最后,别忘了在服务程序的Run()函数中调用CreateMyProcess()函数,并添加一些必要的头文件和库,即可完成。
#include
#include
#pragma comment(lib, "Wtsapi32.lib")
#pragma comment(lib, "Userenv.lib"
试着编译并运行一下该服务吧,如果不出意外,会看到一个记事本程序被启动起来了,看样子一切顺利!但等等,真的一点问题都没有吗?不妨换一个notepad.exe进程之外的进程试试。但在这之前,先让我们准备一个用于测试的小程序,它很简单,仅用几行代码打印出它自己当前工作的目录。
#include
#include
#include
using namespace std;
int main()
{
TCHAR g_strCurrentPath[MAX_PATH] = {0};
GetCurrentDirectory(MAX_PATH, g_strCurrentPath);
wcout << g_strCurrentPath << endl;
system("pause");
return 0;
}
编译这个程序,并尝试在服务中启动它,你会发现它输出了如下的内容:
看这输出,明显不对啊,它的工作目录明明是在C盘的根目录,为什么?仔细看过MSDN上CreateProcessAsUser() API文档的朋友应该知道问题的原因,CreateProcessAsUser() API有一个参数可以用来传递进程当前的工作目录,而在之前我们没有设置该参数!所以我们定义如下的宏,并改正一下CreateProcessAsUser() API的lpCurrentDirectory 参数。
TCHAR szCurrentDirectory[MAX_PATH] = _TEXT("C:\\");
最后完整源码:
#include
#include
#include
#include
#include
#pragma comment(lib, "Wtsapi32.lib")
#pragma comment(lib, "Userenv.lib")
#define LOGFILE "C:\\Msg.log"
TCHAR szApp[MAX_PATH] = _TEXT("C:\\TestApp.exe");
TCHAR szCurrentDirectory[MAX_PATH] = _TEXT("C:\\");
void WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
void WINAPI Handler(DWORD fdwControl);
//日志记录的相关方法
void WriteMsgToLog(const char *Msg);
void WriteErrorToLog(DWORD dwErrorCode, const char *MsgInfo);
//服务相关方法
void InitService();
void Run();
//创建用户GUI进程
void CreateMyProcess();
//全局数据
TCHAR ServiceName[] = _TEXT("ServiceDemo"); //服务名称
SERVICE_STATUS ServiceStatus; //服务状态
SERVICE_STATUS_HANDLE hStatus; //服务状态句柄
int main(int argc, char const *argv[])
{
SERVICE_TABLE_ENTRY ServiceTable[] = {
{ ServiceName, ServiceMain },
{ NULL, NULL },
};
return StartServiceCtrlDispatcher(ServiceTable);
}
void WINAPI ServiceMain(DWORD argc, LPTSTR* argv)
{
ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS;
ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
ServiceStatus.dwWin32ExitCode = NO_ERROR;
ServiceStatus.dwServiceSpecificExitCode = NO_ERROR;
ServiceStatus.dwCheckPoint = 0;
ServiceStatus.dwWaitHint = 0;
hStatus = RegisterServiceCtrlHandler(ServiceName, Handler);
if (!hStatus)
{
DWORD dwError = GetLastError();
WriteErrorToLog(dwError, "RegisterServiceCtrlHandler");
return;
}
InitService();
//设置服务状态
ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hStatus, &ServiceStatus);
Run();
}
void WINAPI Handler(DWORD fdwControl)
{
switch (fdwControl)
{
case SERVICE_CONTROL_STOP:
{
WriteMsgToLog("ServiceDemo 服务停止...");
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = 0;
SetServiceStatus(hStatus, &ServiceStatus);
}
break;
case SERVICE_CONTROL_SHUTDOWN:
{
WriteMsgToLog("ServiceDemo 服务终止...");
ServiceStatus.dwCurrentState = SERVICE_STOPPED;
ServiceStatus.dwWin32ExitCode = 0;
SetServiceStatus(hStatus, &ServiceStatus);
}
break;
default:
break;
}
}
void InitService()
{
WriteMsgToLog("ServiceDemo 服务启动...");
}
void Run()
{
WriteMsgToLog("Hello Win32 Service");
CreateMyProcess();
}
void WriteMsgToLog(const char *Msg)
{
FILE *file = NULL;
fopen_s(&file, LOGFILE, "a+");
if (!file || !Msg)
{
return;
}
fprintf_s(file, "%s\n", Msg);
fflush(file);
fclose(file);
file = NULL;
}
void WriteErrorToLog(DWORD dwErrorCode, const char *MsgInfo)
{
if (!MsgInfo) return;
char Msg[128] = { 0 };
sprintf_s(Msg, "%s Faild: %lu\n", MsgInfo, dwErrorCode);
WriteMsgToLog(Msg);
}
void CreateMyProcess()
{
DWORD dwSessionID = WTSGetActiveConsoleSessionId();
HANDLE hToken = NULL;
HANDLE hTokenDup = NULL;
LPVOID pEnv = NULL;
STARTUPINFO si;
PROCESS_INFORMATION pi;
//获取当前处于活动状态用户的Token
if (!WTSQueryUserToken(dwSessionID, &hToken))
{
DWORD nCode = GetLastError();
WriteErrorToLog(nCode, "WriteErrorToLog");
CloseHandle(hToken);
return;
}
//复制新的Token
if (!DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hTokenDup))
{
DWORD nCode = GetLastError();
WriteErrorToLog(nCode, "DuplicateTokenEx");
CloseHandle(hToken);
return;
}
//创建环境信息
if (!CreateEnvironmentBlock(&pEnv, hTokenDup, FALSE))
{
DWORD nCode = GetLastError();
WriteErrorToLog(nCode, "CreateEnvironmentBlock");
CloseHandle(hTokenDup);
CloseHandle(hToken);
return;
}
//设置启动参数
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
si.lpDesktop = _TEXT("winsta0\\default");
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
//开始创建进程
DWORD dwCreateFlag = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT;
if (!CreateProcessAsUser(hTokenDup, szApp, NULL, NULL, NULL, FALSE, dwCreateFlag, pEnv, szCurrentDirectory, &si, &pi))
{
DWORD nCode = GetLastError();
WriteErrorToLog(nCode, "CreateProcessAsUser");
DestroyEnvironmentBlock(pEnv);
CloseHandle(hTokenDup);
CloseHandle(hToken);
return;
}
//附加操作,回收资源
//等待启动的进程结束
WaitForSingleObject(pi.hProcess, INFINITE);
WriteMsgToLog("子进程结束,回收资源...");
DestroyEnvironmentBlock(pEnv);
CloseHandle(hTokenDup);
CloseHandle(hToken);
}
附加资料:
1.会话0隔离 【点我】
2.CreateProcessAsUser() windowstations and desktops 【点我】