配置BackgroundService在用户会话运行的问题及解决方案咨询
问题概况
基于新版WorkerService模板开发Windows服务,核心监控逻辑使用Process.GetProcesses()枚举进程,筛选条件为:
item.MainWindowTitle.Length > 0 || (item.ProcessName.ToLowerInvariant() == "explorer")
在Visual Studio中调试运行正常,但通过sc create ".T" binPath="..."注册为服务并启动后,仅能检测到explorer进程。已尝试切换服务登录账户(本地系统/指定用户)、使用sc config ".T" obj= "..." password= "..."配置,均未解决问题;旧版Windows服务模板注册失败,优先使用微软推荐的WorkerService+BackgroundService方案。
核心原因
Windows服务默认运行在会话0,这是一个与用户登录会话(通常为会话1及以上)完全隔离的系统会话。即便是本地系统账户,默认也没有权限跨会话访问用户进程的交互相关信息(比如MainWindowTitle依赖进程所在的会话上下文),Process.GetProcesses()在会话0中只能枚举到该会话内的进程,以及少数跨会话的系统进程(如explorer)。
解决方法
1. 配置服务跨会话访问(仅本地系统账户可用,有安全风险)
开启服务与桌面交互权限,允许服务访问用户会话的桌面资源:
- 命令行执行:
sc config ".T" type= own type= interact - 或通过服务属性界面:找到目标服务→右键「属性」→「登录」选项卡→勾选「允许服务与桌面交互」
注意:该功能在Windows Vista及以后版本中被严格限制,且存在安全隐患(恶意程序可利用该权限获取用户桌面控制权),仅建议在测试环境使用。
2. 通过Windows API枚举所有会话的进程
Process.GetProcesses()无法直接获取跨会话进程,需调用Windows原生API枚举所有会话及对应进程,步骤如下:
- 引入P/Invoke调用WTS相关API:
using System.Runtime.InteropServices; using System.Diagnostics; using System.Collections.Generic; public class WtsHelper { [DllImport("wtsapi32.dll")] private static extern bool WTSEnumerateSessions(IntPtr hServer, int Reserved, int Version, out IntPtr ppSessionInfo, out int pCount); [DllImport("wtsapi32.dll")] private static extern void WTSFreeMemory(IntPtr pMemory); [DllImport("wtsapi32.dll")] private static extern bool WTSEnumerateProcesses(IntPtr hServer, int Reserved, int Version, out IntPtr ppProcessInfo, out int pCount); [StructLayout(LayoutKind.Sequential)] private struct WTS_SESSION_INFO { public int SessionId; [MarshalAs(UnmanagedType.LPStr)] public string pWinStationName; public int State; } [StructLayout(LayoutKind.Sequential)] private struct WTS_PROCESS_INFO { public int ProcessId; [MarshalAs(UnmanagedType.LPStr)] public string pProcessName; [MarshalAs(UnmanagedType.LPStr)] public string pUserName; public int SessionId; } public static List<int> GetAllUserSessionIds() { var sessionIds = new List<int>(); IntPtr ppSessionInfo = IntPtr.Zero; int count; if (WTSEnumerateSessions(IntPtr.Zero, 0, 1, out ppSessionInfo, out count)) { for (int i = 0; i < count; i++) { IntPtr currentSession = IntPtr.Add(ppSessionInfo, i * Marshal.SizeOf(typeof(WTS_SESSION_INFO))); var sessionInfo = (WTS_SESSION_INFO)Marshal.PtrToStructure(currentSession, typeof(WTS_SESSION_INFO)); // 仅筛选活跃用户会话(State=0表示已连接) if (sessionInfo.State == 0) { sessionIds.Add(sessionInfo.SessionId); } } WTSFreeMemory(ppSessionInfo); } return sessionIds; } public static List<Process> GetProcessesBySessionId(int sessionId) { var processes = new List<Process>(); IntPtr ppProcessInfo = IntPtr.Zero; int count; if (WTSEnumerateProcesses(IntPtr.Zero, 0, 1, out ppProcessInfo, out count)) { for (int i = 0; i < count; i++) { IntPtr currentProcess = IntPtr.Add(ppProcessInfo, i * Marshal.SizeOf(typeof(WTS_PROCESS_INFO))); var processInfo = (WTS_PROCESS_INFO)Marshal.PtrToStructure(currentProcess, typeof(WTS_PROCESS_INFO)); if (processInfo.SessionId == sessionId) { try { var process = Process.GetProcessById(processInfo.ProcessId); processes.Add(process); } catch { // 跳过无权限访问的进程 continue; } } } WTSFreeMemory(ppProcessInfo); } return processes; } } - 在WorkerService的监控逻辑中使用:
while (/* 你的循环条件 */) { var userSessionIds = WtsHelper.GetAllUserSessionIds(); var allUserProcesses = new List<Process>(); foreach (var sessionId in userSessionIds) { allUserProcesses.AddRange(WtsHelper.GetProcessesBySessionId(sessionId)); } foreach (var item in allUserProcesses) { // 你的筛选逻辑 if (item.MainWindowTitle.Length > 0 || (item.ProcessName.ToLowerInvariant() == "explorer")) { // 处理逻辑 } } await Task.Delay(1000); }
替代方案:无UI控制台程序+任务计划
如果不想处理Windows服务的会话隔离问题,可以改用无UI控制台程序,配合任务计划程序实现后台运行:
- 编写控制台程序,逻辑与原WorkerService一致(无需继承BackgroundService,直接在Main方法中写循环监控逻辑)
- 打开「任务计划程序」→创建基本任务:
- 触发器选择「当用户登录时」
- 操作选择「启动程序」,指向你的控制台程序
- 勾选「隐藏」选项,实现无后台运行
- 该程序运行在用户会话中,可正常获取所有用户进程,且能自由写入文件,无需处理会话0的隔离限制
内容的提问来源于stack exchange,提问作者The Crazy

