You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多用户登录时SYSTEM级外部程序桌面显示异常问题求助

问题成因与解决方法

兄弟,这个问题我太熟了——之前做企业应用的多会话部署时踩过一模一样的坑!咱们先把问题根源说清楚,再给你改代码。

一、问题到底出在哪?

你的代码里有两个关键的错误点,导致多用户登录时进程跑错桌面:

  1. 错误的会话ID获取方式
    你用WtsGetActiveConsoleSessionID()拿到的是物理控制台(显示器)当前绑定的会话ID,不是当前运行你程序的用户会话ID。比如先登录user1(会话0,绑定控制台),再切换到user2(会话1,用快速用户切换或远程桌面),此时控制台依然绑定user1的会话,所以你拿到的token是user1的,启动的进程自然出现在user1桌面。

  2. 错误的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:57:43