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

Windows 7服务中基于C#实现会话登录/注销触发的问题排查

嘿,我太懂你这种卡壳的感觉了——之前用循环检测explorer.exe导致服务一直卡在“启动中”,转用会话变更事件又摸不着门道,这事儿确实折腾人。我来给你拆解一下正确的实现路径,帮你把这个Windows服务搞定。

先说说为什么循环检测会卡死服务

Windows服务的OnStart方法有严格的超时限制(默认30秒),如果在这个方法里搞无限循环,会直接阻塞服务的启动流程,系统就会判定服务启动失败,一直显示“启动中”。这个思路从根上就走不通,必须放弃。

会话变更事件的正确打开方式

你选的方向是对的,C#的ServiceBase提供了会话变更事件的处理能力,但关键是要配置正确,还要解决跨会话启动进程的核心问题(服务默认跑在会话0,用户登录在其他会话,直接启动进程用户看不到)。

完整实现代码示例

下面是可直接参考的服务代码,包含登录启动、注销停止进程的逻辑,还处理了跨会话启动的问题:

using System;
using System.Collections.Generic;
using System.ServiceProcess;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Linq;

namespace SessionProcessService
{
    public partial class SessionProcessService : ServiceBase
    {
        // 记录每个会话对应的进程ID,方便注销时精准终止
        private Dictionary<int, int> _sessionProcessMap = new Dictionary<int, int>();

        public SessionProcessService()
        {
            InitializeComponent();
            // 必须开启这个属性,才能接收会话变更通知
            this.CanHandleSessionChangeEvent = true;
        }

        protected override void OnStart(string[] args)
        {
            // OnStart里只做轻量初始化,绝对不能阻塞
            EventLog.WriteEntry("SessionProcessService", "服务已启动,等待会话事件");
        }

        protected override void OnSessionChange(SessionChangeDescription changeDescription)
        {
            base.OnSessionChange(changeDescription);
            int sessionId = changeDescription.SessionId;

            switch (changeDescription.Reason)
            {
                case SessionChangeReason.SessionLogon:
                    // 用户登录,启动目标进程
                    StartTargetProcess(sessionId);
                    break;
                case SessionChangeReason.SessionLogoff:
                    // 用户注销,停止对应会话的进程
                    StopTargetProcess(sessionId);
                    break;
            }
        }

        private void StartTargetProcess(int sessionId)
        {
            try
            {
                if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Core") || 
                    RuntimeInformation.FrameworkDescription.StartsWith(".NET 5"))
                {
                    // .NET Core/5+ 直接支持设置SessionId,简单省心
                    var startInfo = new ProcessStartInfo
                    {
                        FileName = "notepad.exe", // 替换成你要启动的进程路径
                        UseShellExecute = false,
                        CreateNoWindow = false,
                        SessionId = sessionId
                    };

                    var process = Process.Start(startInfo);
                    if (process != null)
                    {
                        _sessionProcessMap[sessionId] = process.Id;
                        EventLog.WriteEntry("SessionProcessService", $"会话{sessionId}登录,启动进程PID:{process.Id}");
                    }
                }
                else
                {
                    // .NET Framework 需要用P/Invoke调用CreateProcessAsUser(跨会话启动必备)
                    StartProcessAsUserInSession(sessionId, "C:\\Windows\\notepad.exe");
                }
            }
            catch (Exception ex)
            {
                EventLog.WriteEntry("SessionProcessService", $"启动进程失败:{ex.Message}", EventLogEntryType.Error);
            }
        }

        private void StopTargetProcess(int sessionId)
        {
            if (_sessionProcessMap.TryGetValue(sessionId, out int pid))
            {
                try
                {
                    var process = Process.GetProcessById(pid);
                    if (!process.HasExited)
                    {
                        process.Kill();
                        process.WaitForExit(5000);
                        EventLog.WriteEntry("SessionProcessService", $"会话{sessionId}注销,终止进程PID:{pid}");
                    }
                    _sessionProcessMap.Remove(sessionId);
                }
                catch (Exception ex)
                {
                    EventLog.WriteEntry("SessionProcessService", $"终止进程失败:{ex.Message}", EventLogEntryType.Error);
                    _sessionProcessMap.Remove(sessionId);
                }
            }
        }

        // .NET Framework 专用的跨会话启动进程实现
        private void StartProcessAsUserInSession(int sessionId, string processPath)
        {
            IntPtr userToken = IntPtr.Zero;
            if (WTSQueryUserToken(sessionId, out userToken))
            {
                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);

                if (success)
                {
                    _sessionProcessMap[sessionId] = pi.dwProcessId;
                    EventLog.WriteEntry("SessionProcessService", $"会话{sessionId}登录,启动进程PID:{pi.dwProcessId}");
                    // 记得关闭句柄,避免资源泄漏
                    CloseHandle(pi.hProcess);
                    CloseHandle(pi.hThread);
                }
                else
                {
                    int errorCode = Marshal.GetLastWin32Error();
                    EventLog.WriteEntry("SessionProcessService", $"跨会话启动失败,错误码:{errorCode}", EventLogEntryType.Error);
                }
                CloseHandle(userToken);
            }
            else
            {
                int errorCode = Marshal.GetLastWin32Error();
                EventLog.WriteEntry("SessionProcessService", $"获取用户Token失败,错误码:{errorCode}", EventLogEntryType.Error);
            }
        }

        protected override void OnStop()
        {
            // 服务停止时,清理所有已启动的进程
            foreach (var kvp in _sessionProcessMap.ToList())
            {
                StopTargetProcess(kvp.Key);
            }
            EventLog.WriteEntry("SessionProcessService", "服务已停止");
        }

        // P/Invoke 所需的API声明
        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);

        [DllImport("kernel32.dll")]
        private static extern bool CloseHandle(IntPtr hObject);

        [DllImport("Wtsapi32.dll")]
        private static extern bool WTSQueryUserToken(int sessionId, out IntPtr phToken);

        [StructLayout(LayoutKind.Sequential)]
        private 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)]
        private struct PROCESS_INFORMATION
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public int dwProcessId;
            public int dwThreadId;
        }
    }
}

关键注意事项

  1. 权限配置:服务需要足够的权限才能跨会话启动进程,建议给服务分配“本地系统”账户,或者具备SeIncreaseQuotaPrivilege和SeAssignPrimaryTokenPrivilege权限的账户。
  2. 测试方式:不要直接在VS里调试服务,要通过installutil命令安装服务后,在服务管理器中启动,避免调试环境的会话隔离问题。
  3. 日志排查:用EventLog记录关键操作和错误,比写文本文件更可靠,直接在事件查看器的“应用程序日志”里就能看到详情。
  4. 避免阻塞:OnSessionChange方法里也不能做长时间阻塞操作,所有耗时逻辑要异步处理(比如启动进程可以用Task.Run包裹)。

内容的提问来源于stack exchange,提问作者user2551208

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 03:43:25