能否从Windows Service中为已登录用户打开Notepad?
从Windows Service为已登录用户启动应用的解决方案
Windows从Vista开始引入了Session 0隔离机制:系统服务默认运行在Session 0,而登录用户的桌面会话在Session 1及以后,直接在服务中启动程序会跑到Session 0,用户无法看到。你之前尝试的“允许与桌面交互”选项仅对旧系统有效,现在必须通过API在用户的会话中启动程序。
核心实现步骤
获取当前活跃用户的会话ID
使用WTSGetActiveConsoleSessionIdAPI获取当前正在使用控制台的用户会话ID(多用户登录时返回活跃桌面的会话)。获取用户会话令牌
通过WTSQueryUserTokenAPI传入会话ID,获取该用户的访问令牌。启用服务所需权限
服务需要SeAssignPrimaryTokenPrivilege和SeIncreaseQuotaPrivilege权限,需先通过API启用这些权限。在用户会话中启动程序
调用CreateProcessAsUserAPI,传入用户令牌和程序路径,指定用户桌面环境(winsta0\\default)启动程序。
C#代码实现(适配ASP.NET托管的gRPC服务)
以下是封装好的工具类和调用示例:
using System; using System.Runtime.InteropServices; using System.Diagnostics; public class UserSessionHelper { [DllImport("kernel32.dll")] public static extern uint WTSGetActiveConsoleSessionId(); [DllImport("wtsapi32.dll", SetLastError = true)] public static extern bool WTSQueryUserToken(uint sessionId, out IntPtr token); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool CreateProcessAsUser( IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; } [DllImport("advapi32.dll", SetLastError = true)] public static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, uint BufferLength, IntPtr PreviousState, IntPtr ReturnLength); public struct LUID { public uint LowPart; public int HighPart; } public struct TOKEN_PRIVILEGES { public uint PrivilegeCount; public LUID Luid; public uint Attributes; } private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; private const uint TOKEN_QUERY = 0x0008; private const uint SE_PRIVILEGE_ENABLED = 0x00000002; private const string SE_ASSIGNPRIMARYTOKEN_NAME = "SeAssignPrimaryTokenPrivilege"; private const string SE_INCREASE_QUOTA_NAME = "SeIncreaseQuotaPrivilege"; public static bool EnablePrivileges() { if (!OpenProcessToken(Process.GetCurrentProcess().Handle, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out IntPtr tokenHandle)) return false; bool result = true; // 启用SeAssignPrimaryTokenPrivilege if (!LookupPrivilegeValue(null, SE_ASSIGNPRIMARYTOKEN_NAME, out LUID luid1)) result = false; TOKEN_PRIVILEGES tp1 = new TOKEN_PRIVILEGES(); tp1.PrivilegeCount = 1; tp1.Luid = luid1; tp1.Attributes = SE_PRIVILEGE_ENABLED; if (!AdjustTokenPrivileges(tokenHandle, false, ref tp1, 0, IntPtr.Zero, IntPtr.Zero)) result = false; // 启用SeIncreaseQuotaPrivilege if (!LookupPrivilegeValue(null, SE_INCREASE_QUOTA_NAME, out LUID luid2)) result = false; TOKEN_PRIVILEGES tp2 = new TOKEN_PRIVILEGES(); tp2.PrivilegeCount = 1; tp2.Luid = luid2; tp2.Attributes = SE_PRIVILEGE_ENABLED; if (!AdjustTokenPrivileges(tokenHandle, false, ref tp2, 0, IntPtr.Zero, IntPtr.Zero)) result = false; Marshal.Close(tokenHandle); return result; } public static bool StartProcessInUserSession(string processPath) { if (!EnablePrivileges()) return false; uint sessionId = WTSGetActiveConsoleSessionId(); if (sessionId == 0xFFFFFFFF) return false; if (!WTSQueryUserToken(sessionId, out IntPtr userToken)) return false; STARTUPINFO si = new STARTUPINFO(); si.cb = Marshal.SizeOf(si); si.lpDesktop = "winsta0\\default"; // 指定用户桌面会话 PROCESS_INFORMATION pi = new PROCESS_INFORMATION(); bool success = CreateProcessAsUser( userToken, processPath, null, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, null, ref si, out pi); Marshal.Close(userToken); if (success) { Marshal.Close(pi.hProcess); Marshal.Close(pi.hThread); } return success; } }
在gRPC服务中调用
// 启动记事本示例 bool isStarted = UserSessionHelper.StartProcessInUserSession("notepad.exe"); // 可根据返回值判断是否启动成功
注意事项
- 服务权限配置:若使用Local System账户启动服务,默认已具备所需权限;若使用普通用户账户,需通过组策略(
gpedit.msc)给该用户分配替换进程级令牌和调整进程内存配额权限。 - 多用户场景:如果有多个用户同时登录,
WTSGetActiveConsoleSessionId返回的是当前操作控制台的用户会话,若需针对特定用户,需枚举所有会话并筛选目标用户。 - 错误处理:实际使用中需添加
Marshal.GetLastWin32Error()获取具体错误码,排查启动失败原因。
内容的提问来源于stack exchange,提问作者Álvaro García
相关产品推荐
相关产品推荐

