最近项目中需要在windows服务中启动交互式程序,还是费了点劲,记录一下
交互式程序也就是需要与人交互的程序,比如带有界面的程序,需要接收用户鼠标键盘消息的程序。比如 notepad.exe,cmd.exe
非交互式程序也就是跑在后台,不需要人工干预,自己能跑得欢的程序 比如各种服务端程序,类似sshd.exe,各种bat脚本等
windows程序中启动第三方程序通常有几种方式
1)UINT Win Exec(LPCSTR lpCmdLine, UINT uCmdShow); 用得很少
WinExec("Notepad.exe",SW_SHOW);//启动Notepad.exe
2)int system(char const* _Command);
system("dir >dir.txt");//把当前目录的内容写入dir.txt
3)HINSTANCE ShellExecute(HWND hwnd, LPCTSTR lpOperation, LPCTSTR lpFile, LPCTSTR lpParameters, LPCTSTR lpDirectory, INT nShowCmd);
ShellExecute (NULL,”open”,”readme.txt”,NULL,NULL,SW_SHOW);//打开readme.txt
4)BOOL CreateProcessA(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
lpApplicationName,要启动的应用程序名,如果为NULL,则取lpCommandLine以空格分隔的第一个参数为程序名
lpCOmmandLine,命令行参数,如果为NULL,则用lpApplicationName为命令行参数
lpProcessAttributes,指向一个SECURITY_ATTRIBUTES结构体,这个结构体决定是否返回的句柄可以被子进程继承,如果lpProcessAttributes参数为空(NULL),那么句柄不能被继承。
lpThreadAttributes,同lpProcessAttribute,不过这个参数决定的是线程是否被继承.通常置为NULL.
bInheritHandles,指示新进程是否从调用进程处继承了句柄
dwCreationFlags,指定附加的、用来控制优先类和进程的创建的标志。
lpEnvironment,指向一个新进程的环境块。如果此参数为空,新进程使用调用进程的环境。
lpCurrentDirectory,指向一个以NULL结尾的字符串,这个字符串用来指定子进程的工作路径。这个字符串必须是一个包含驱动器名的绝对路径。如果这个参数为空,新进程将使用与调用进程相同的驱动器和目录。
lpStartupInfo,指向一个用于决定新进程的主窗体如何显示的STARTUPINFO结构体。
lpProcessInformation,指向一个用来接收新进程的识别信息的PROCESS_INFORMATION结构体。
以下这段解释来自于 https://www.jb51.net/article/82757.htm,解释为什么服务里无法启动带交互式界面的程序
Windows提供了三类对象:用户界面对象(User Interface)、GDI对象和内核对象。内核对象有安全性,而前两者没有。为了对前两者提供安全性,通过工作站对象(Window station)和桌面对象(Desktop)来管理用户界面对象,因为工作站对象和桌面对象有安全特性。简单说来,工作站是一个带有安全特性的对象,它与进程相关联,包含了一个或多个桌面对象。当工作站对象被创建时,它被关联到调用进程上,并且被赋给当前Session。交互式工作站WinSta0,是唯一一个可以显示用户界面,接受用户输入的工作站。它被赋给交互式用户的登录Session,包含了键盘、鼠标和显示设备。所有其他工作站都是非交互式的,这就意味着它们不能显示用户界面,不能接受用户的输入。当用户登录到一台启用了终端服务的计算机上时,每个用户都会启动一个Session。每个Session都会与自己的交互式工作站相联系。桌面是一个带有安全特性的对象,被包含在一个窗口工作站对象中。一个桌面对象有一个逻辑的显示区域,包含了诸如窗口、菜单、钩子等等这样的用户界面对象。
在Vista之前,之所以可以通过打开Winsta0和缺省桌面显示对话框,是因为不管是服务还是第一个登录的交互式用户,都是登录到Session 0中。因此,服务程序可以通过强制打开WinSta0和桌面来获得交互能力。
然而,在Vista和Windows2008中,Session 0专用于服务和其他不与用户交互的应用程序。第一个登录进来,可以进行交互式操作的用户被连到Session 1上。第二个登录进行的用户被分配给Session 2,以此类推。Session 0完全不支持要与用户交互的进程。如果采取在服务进程中启动子进程来显示对话框,子对话框将无法显示;如果采取用OpenWindowStation系统API打开WinSta0的方法,函数调用会失败。总之,Vista和Windows2008已经堵上了在Session 0中产生界面交互的路。这就是原因所在。
那么,是否真的没法在服务中弹出对话框了呢?对于服务进程自身来说,确实如此,操作系统已经把这条路堵上了。但是,我们想要的并不是“在服务进程中弹出对话框”,我们想要的不过是“当服务出现某些状况的时候,在桌面上弹出对话框”。既然在Session 0中无法弹出对话框,而我们看到的桌面是Session X,并非Session 0,很自然的一个想法是:能不能让Session 0通知其他的Session,让当前桌面正显示着的Session弹一个对话框呢?
事实上是可以的,那么,就需要获取当前登录进来的用户session X
代码如下
BOOL CreateMyProcess3(LPWSTR command_line, DWORD *exit_code)
{
PROCESSENTRY32 proc_entry;
DWORD session_id ;
HANDLE hToken = NULL;
HANDLE hTokenDup = NULL;
LPVOID pEnv = NULL;
STARTUPINFO si;
PROCESS_INFORMATION pi;
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) {
printf("CreateToolhelp32Snapshot() failed %lu", GetLastError());
return FALSE;
}
ZeroMemory(&proc_entry, sizeof(proc_entry));
proc_entry.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(snap, &proc_entry)) {
printf("Process32First() failed %lu", GetLastError());
CloseHandle(snap);
return FALSE;
}
do {
// 找出explorer进程所属用户的sessionID
if (_tcsicmp(proc_entry.szExeFile, _T("explorer.exe")) == 0) {
DWORD explorer_session_id = 0;
if (ProcessIdToSessionId(proc_entry.th32ProcessID, &explorer_session_id) )
{
session_id = explorer_session_id;
break;
}
}
} while (Process32Next(snap, &proc_entry));
CloseHandle(snap);
// 根据sessionID获取Token
if (!WTSQueryUserToken(session_id , &hToken))
{
DWORD nCode = GetLastError();
printf("WTSQueryUserToken error [%d]", nCode);
CloseHandle(hToken);
return FALSE;
}
//复制新的Token
if (!DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hTokenDup))
{
DWORD nCode = GetLastError();
printf("DuplicateTokenEx error [%d]", nCode);
*exit_code = nCode;
CloseHandle(hToken);
return FALSE;
}
//创建环境信息
if (!CreateEnvironmentBlock(&pEnv, hTokenDup, FALSE))
{
DWORD nCode = GetLastError();
printf("CreateEnvironmentBlock error [%d]", nCode);
*exit_code = nCode;
CloseHandle(hTokenDup);
CloseHandle(hToken);
return FALSE;
}
//设置启动参数
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(hToken, NULL, command_line, NULL, NULL, FALSE, dwCreateFlag, pEnv, NULL, &si, &pi))
{
DWORD nCode = GetLastError();
printf("CreateProcessAsUser error [%d]", nCode);
*exit_code = nCode;
DestroyEnvironmentBlock(pEnv);
CloseHandle(hTokenDup);
CloseHandle(hToken);
return FALSE;
}
DestroyEnvironmentBlock(pEnv);
CloseHandle(hTokenDup);
CloseHandle(hToken);
return TRUE;
}
这样,新启动的程序就可以接收键盘鼠标输入,来进行交互了