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

能否从Windows Service中为已登录用户打开Notepad?

从Windows Service为已登录用户启动应用的解决方案

Windows从Vista开始引入了Session 0隔离机制:系统服务默认运行在Session 0,而登录用户的桌面会话在Session 1及以后,直接在服务中启动程序会跑到Session 0,用户无法看到。你之前尝试的“允许与桌面交互”选项仅对旧系统有效,现在必须通过API在用户的会话中启动程序。

核心实现步骤

  1. 获取当前活跃用户的会话ID
    使用WTSGetActiveConsoleSessionId API获取当前正在使用控制台的用户会话ID(多用户登录时返回活跃桌面的会话)。

  2. 获取用户会话令牌
    通过WTSQueryUserToken API传入会话ID,获取该用户的访问令牌。

  3. 启用服务所需权限
    服务需要SeAssignPrimaryTokenPrivilege和SeIncreaseQuotaPrivilege权限,需先通过API启用这些权限。

  4. 在用户会话中启动程序
    调用CreateProcessAsUser API,传入用户令牌和程序路径,指定用户桌面环境(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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 12:01:03