多用户登录时SYSTEM级外部程序桌面显示异常问题求助
问题成因与解决方法
兄弟,这个问题我太熟了——之前做企业应用的多会话部署时踩过一模一样的坑!咱们先把问题根源说清楚,再给你改代码。
一、问题到底出在哪?
你的代码里有两个关键的错误点,导致多用户登录时进程跑错桌面:
错误的会话ID获取方式
你用WtsGetActiveConsoleSessionID()拿到的是物理控制台(显示器)当前绑定的会话ID,不是当前运行你程序的用户会话ID。比如先登录user1(会话0,绑定控制台),再切换到user2(会话1,用快速用户切换或远程桌面),此时控制台依然绑定user1的会话,所以你拿到的token是user1的,启动的进程自然出现在user1桌面。错误的Winlogon进程获取
每个用户会话都有自己的winlogon.exe进程,你用ProcessIDFromAppname32('winlogon.exe')只会拿到第一个启动的winlogon(也就是user1的),后续CreateProcessAsUser用这个进程的token,直接把进程开到user1的会话里了。
二、怎么解决?
核心思路是:绑定到当前程序运行的用户会话,而不是控制台会话。具体要改这几点:
- 获取当前进程所在的会话ID(也就是user2的会话)
- 根据这个会话ID获取对应用户的token
- 找到该会话专属的Winlogon进程,用它的token来启动程序
- 补全
STARTUPINFO的必填字段(之前漏了cb,会导致调用失败)
修改后的关键代码
首先,在TestSystem.pas里添加缺失的API声明:
function ProcessIdToSessionId(dwProcessId: DWORD; pSessionId: PDWORD): BOOL; stdcall; external 'kernel32.dll';
然后实现一个根据会话ID找对应Winlogon进程的函数:
function GetWinlogonProcessIdBySession(SessionId: DWORD): DWORD; var Snapshot: THandle; ProcessEntry: TProcessEntry32; ProcessSessionId: DWORD; begin Result := 0; Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if Snapshot <> INVALID_HANDLE_VALUE then try ProcessEntry.dwSize := Sizeof(ProcessEntry); if Process32First(Snapshot, ProcessEntry) then repeat if UpperCase(ExtractFilename(StrPas(ProcessEntry.szExeFile))) = 'WINLOGON.EXE' then begin if ProcessIdToSessionId(ProcessEntry.th32ProcessID, @ProcessSessionId) then begin if ProcessSessionId = SessionId then begin Result := ProcessEntry.th32ProcessID; Break; end; end; end; until not Process32Next(Snapshot, ProcessEntry); finally CloseHandle(Snapshot); end; end;
最后修改TTestService.ServiceExecute过程,替换原来的逻辑:
procedure TTestService.ServiceExecute(Sender: TService); var hToken, hUserToken: THandle; StartupInfo : TStartupInfoW; ProcessInfo : TProcessInformation; P : Pointer; CurrentSessionId: DWORD; WinlogonPID: DWORD; begin // 第一步:获取当前进程所在的会话ID(也就是调用程序的用户会话) if not ProcessIdToSessionId(GetCurrentProcessId, @CurrentSessionId) then exit; // 第二步:根据会话ID获取该用户的token if not WTSQueryUserToken(CurrentSessionId, hUserToken) then exit; // 第三步:找到对应会话的Winlogon进程ID WinlogonPID := GetWinlogonProcessIdBySession(CurrentSessionId); if WinlogonPID = 0 then exit; // 第四步:打开该Winlogon进程的token(注意权限要足够) if not OpenProcessToken(OpenProcess(PROCESS_QUERY_INFORMATION, False, WinlogonPID), TOKEN_DUPLICATE or TOKEN_ASSIGN_PRIMARY or TOKEN_QUERY, hToken) then exit; if CreateEnvironmentBlock(P, hUserToken, True) then begin ZeroMemory(@StartupInfo, sizeof(StartupInfo)); StartupInfo.cb := sizeof(StartupInfo); // 必须设置这个字段,否则CreateProcessAsUser会失败 StartupInfo.lpDesktop := PWideChar('winsta0\default'); StartupInfo.wShowWindow := SW_SHOWNORMAL; // 启动进程,加上CREATE_NEW_CONSOLE避免和服务控制台混在一起 if CreateProcessAsUser(hToken, lpApplicationName, lpCommandLine, nil, nil, False, CREATE_UNICODE_ENVIRONMENT or CREATE_NEW_CONSOLE, P, lpCurrentDirectory, StartupInfo, ProcessInfo) then begin // 这里可以加成功后的日志或处理逻辑 end; CloseHandle(ProcessInfo.hProcess); CloseHandle(ProcessInfo.hThread); DestroyEnvironmentBlock(P); end; CloseHandle(hToken); CloseHandle(hUserToken); TerminateProcessByID(GetCurrentProcessId); end;
三、额外注意事项
- 确保你的服务有足够的权限:需要
SeAssignPrimaryTokenPrivilege和SeIncreaseQuotaPrivilege权限,否则CreateProcessAsUser会失败。你可以在服务启动时启用这些权限。 - 如果是远程桌面会话,
winsta0\default依然是正确的桌面路径,不用改。 - 测试时记得先卸载旧服务,再安装修改后的新服务,避免缓存问题。
内容的提问来源于stack exchange,提问作者Leong
相关产品推荐
相关产品推荐

