惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
C
Check Point Blog
B
Blog RSS Feed
G
Google Developers Blog
H
Help Net Security
博客园 - Franky
Blog — PlanetScale
Blog — PlanetScale
H
Hackread – Cybersecurity News, Data Breaches, AI and More
量子位
Recent Announcements
Recent Announcements
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
D
DataBreaches.Net
小众软件
小众软件
T
The Blog of Author Tim Ferriss
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
MongoDB | Blog
MongoDB | Blog
Y
Y Combinator Blog
T
Tailwind CSS Blog
J
Java Code Geeks
MyScale Blog
MyScale Blog
雷峰网
雷峰网
有赞技术团队
有赞技术团队
博客园 - 聂微东

博客园 - VAllen

ConcurrentNativeQueue<T>:一个使用 .NET 实现的零 GC 压力的无锁 MPSC 原生队列 在DOS环境下升级BMC带外管理模块固件的另类办法 .NET 响应式编程 System.Reactive 系列文章(三):Subscribe 和 IDisposable 的深入理解 .NET 响应式编程 System.Reactive 系列文章(二):深入理解 IObservable<T> 和 IObserver<T> .NET 响应式编程 System.Reactive 系列文章(一):基础概念 分析基于ASP.NET Core Kernel的gRPC服务在不同.NET版本的不同部署方式的不同线程池下的性能表现 从Windows 11 23H2升级至24H2后,Git操作提示文件所有权错误的3种有效解决方案 在Windows平台使用源码编译和安装PyTorch3D指定版本 【一天一点.NET小知识】运用向量Vector<T>加速求和计算 为什么不推荐使用Linq? 如何使用csproj构建C#源代码组件NuGet包? 如何更改.NET中的默认时区? 高性能版本的零内存分配LikeString函数(ZeroMemAllocLikeOperator) 在.NET Core,除了VB的LikeString,还有其它方法吗?(四种LikeString实现分享) 在 Windows 11 中为 WSL2 启用 Systemd 以及修复ping不通和DNS无法解析等的问题 在Linux平台下使用.NET Core访问Access数据库读取mdb文件数据 Windows 11 + Samsung 980 踩坑:在 LocalDB 15.0 实例启动期间出错: 无法启动 SQL Server 进程(附赠 查询指定日期范围内的前1000条SQL执行记录) SQL Server 2000 创建角色,登陆用户,安全用户,批量授予权限 在PowerShell脚本中获取程序集文件属性的指定元数据特性的方法——AssemblyMetadataAttribute
在System身份运行的.NET程序中以指定的用户身份启动可交互式进程
VAllen · 2024-06-20 · via 博客园 - VAllen

今天在技术群里,石头哥向大家提了个问题:"如何在一个以System身份运行的.NET程序(Windows Services)中,以其它活动的用户身份启动可交互式进程(桌面应用程序、控制台程序、等带有UI和交互式体验的程序)"?

我以前有过类似的需求,是在GitLab流水线中运行带有UI的自动化测试程序

其中流水线是GitLab Runner执行的,而GitLab Runner则被注册为Windows服务,以System身份启动的。

然后我在流水线里,巴拉巴拉写了一大串PowerShell脚本代码,通过调用任务计划程序实现了这个需求

但我没试过在C#里实现这个功能。

对此,我很感兴趣,于是着手研究,最终捣鼓出来了。

二话不多说,上代码:

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using Microsoft.Win32.SafeHandles;

namespace AllenCai.Windows
{
    /// <summary>
    /// 进程工具类
    /// </summary>
#if NET5_0_OR_GREATER
    [SupportedOSPlatform("windows")]
#endif
    public static class ProcessUtils
    {
        /// <summary>
        /// 在当前活动的用户会话中启动进程
        /// </summary>
        /// <param name="fileName">程序名称或程序路径</param>
        /// <param name="commandLine">命令行参数</param>
        /// <param name="workDir">工作目录</param>
        /// <param name="noWindow">是否无窗口</param>
        /// <param name="minimize">是否最小化</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ApplicationException"></exception>
        /// <exception cref="Win32Exception"></exception>
        public static int StartProcessAsActiveUser(string fileName, string commandLine = null, string workDir = null, bool noWindow = false, bool minimize = false)
        {
            if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentNullException(nameof(fileName));

            // 获取当前活动的控制台会话ID和安全的用户访问令牌
            IntPtr userToken = GetSessionUserToken();
            if (userToken == IntPtr.Zero)
                throw new ApplicationException("Failed to get user token for the active session.");

            IntPtr duplicateToken = IntPtr.Zero;
            IntPtr environmentBlock = IntPtr.Zero;
            try
            {
                // 复制令牌
                SecurityAttributes sa = new SecurityAttributes();
                sa.Length = Marshal.SizeOf(sa);
                if (!DuplicateTokenEx(userToken, MAXIMUM_ALLOWED, ref sa, SecurityImpersonationLevel.SecurityIdentification, TokenType.TokenPrimary, out duplicateToken))
                    throw new ApplicationException("Could not duplicate token.");

                // 创建环境块(检索该用户的环境变量)
                if (!CreateEnvironmentBlock(out environmentBlock, duplicateToken, false))
                    throw new ApplicationException("Could not create environment block.");

                bool theCommandIsInPath;
                // 如果文件名不包含路径分隔符,则尝试先在workDir参数中查找。如果找不到,再在指定用户会话的PATH环境变量中查找。如果还是找不到,则抛出异常
                if ((!fileName.Contains('/') && !fileName.Contains('\\')))
                {
                    if (!string.IsNullOrEmpty(workDir))
                    {
                        if (File.Exists(Path.Combine(workDir, fileName)))
                        {
                            // 在指定的工作目录中找到可执行命令文件
                            theCommandIsInPath = false;
                        }
                        else
                        {
                            // 在指定的工作目录(workDir)中找不到可执行命令文件,再在指定用户会话的PATH环境变量中查找。如果还是找不到,则抛出异常
                            if (!InPathOfSpecificUserEnvironment(in duplicateToken, in environmentBlock, fileName))
                            {
                                throw new ApplicationException($"The file '{fileName}' was not found in the specified directory '{workDir}' or in the PATH environment variable.");
                            }
                            else
                            {
                                // 在指定用户会话的PATH环境变量中找到可执行命令文件
                                theCommandIsInPath = true;
                            }
                        }
                    }
                    else
                    {
                        // 在指定用户会话的PATH环境变量中查找,如果找不到,则抛出异常
                        if (!InPathOfSpecificUserEnvironment(in duplicateToken, in environmentBlock, fileName))
                        {
                            throw new ApplicationException($"The file '{fileName}' was not found in the PATH environment variable.");
                        }
                        // 在指定用户会话的PATH环境变量中找到可执行命令文件
                        theCommandIsInPath = true;
                    }
                }
                else
                {
                    theCommandIsInPath = false;
                }

                string file;
                if (!theCommandIsInPath && !Path.IsPathRooted(fileName))
                {
                    file = !string.IsNullOrEmpty(workDir) ? Path.GetFullPath(Path.Combine(workDir, fileName)) : Path.GetFullPath(fileName);
                }
                else
                {
                    file = fileName;
                }

                if (string.IsNullOrWhiteSpace(workDir)) workDir = theCommandIsInPath ? Environment.CurrentDirectory : Path.GetDirectoryName(file);

                if (string.IsNullOrWhiteSpace(commandLine)) commandLine = "";

                // 启动信息
                ProcessStartInfo psi = new ProcessStartInfo
                {
                    UseShellExecute = true,
                    FileName = $"{file} {commandLine}", //解决带参数的进程起不来或者起来的进程没有参数的问题
                    Arguments = commandLine,
                    WorkingDirectory = workDir,
                    RedirectStandardError = false,
                    RedirectStandardOutput = false,
                    RedirectStandardInput = false,
                    CreateNoWindow = noWindow,
                    WindowStyle = minimize ? ProcessWindowStyle.Minimized : ProcessWindowStyle.Normal
                };

                // 在指定的用户会话中创建进程
                SecurityAttributes saProcessAttributes = new SecurityAttributes();
                SecurityAttributes saThreadAttributes = new SecurityAttributes();
                CreateProcessFlags createProcessFlags = (noWindow ? CreateProcessFlags.CREATE_NO_WINDOW : CreateProcessFlags.CREATE_NEW_CONSOLE) | CreateProcessFlags.CREATE_UNICODE_ENVIRONMENT;
                bool success = CreateProcessAsUser(duplicateToken, null, $"{file} {commandLine}", ref saProcessAttributes, ref saThreadAttributes, false, createProcessFlags, environmentBlock, null, ref psi, out ProcessInformation pi);
                if (!success)
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                    //throw new ApplicationException("Could not create process as user.");
                }

                return pi.dwProcessId;
            }
            finally
            {
                // 清理资源
                if (userToken != IntPtr.Zero) CloseHandle(userToken);
                if (duplicateToken != IntPtr.Zero) CloseHandle(duplicateToken);
                if (environmentBlock != IntPtr.Zero) DestroyEnvironmentBlock(environmentBlock);
            }
        }

        /// <summary>
        /// 使用win32api实现在指定用户身份的环境变量中查找命令(command参数)是否存在。
        /// </summary>
        private static bool InPathOfSpecificUserEnvironment(in IntPtr userToken, in IntPtr environmentBlock, in string command)
        {
            // 在指定用户会话环境中执行命令,并且获得控制台标准输出内容
            string commandLine = $"cmd.exe /c chcp 65001 && where {command}";
            string output = ExecuteCommandAsUserAndReturnStdOutput(userToken, environmentBlock, commandLine, Encoding.UTF8);

            // OperatingSystem.IsOSPlatform("WINDOWS") 该方法仅在 .NET Core及以上版本可用,在 .NET Standard 和 .NET Framework 中不可用。
            // 现有操作系统中,Windows 操作系统的目录分隔符为 '\',而 Unix 操作系统的目录分隔符为 '/',因此可以用它来判断和区分操作系统。
            // 如果是Windows操作系统,则不区分大小写
            var comparison = Path.DirectorySeparatorChar == '\\' ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
            return output.IndexOf(command, comparison) >= 0;
        }

        /// <summary>
        /// 在指定用户会话环境中执行命令,并且返回控制台标准输出内容
        /// </summary>
        private static string ExecuteCommandAsUserAndReturnStdOutput(in IntPtr userToken, in IntPtr environmentBlock, string commandLine, Encoding encoding)
        {
            // 创建匿名管道
            var saPipeAttributes = new SecurityAttributes();
            saPipeAttributes.Length = Marshal.SizeOf(saPipeAttributes);
            saPipeAttributes.InheritHandle = true; // 允许句柄被继承
                                                   //saPipeAttributes.SecurityDescriptor = IntPtr.Zero;
            if (!CreatePipe(out IntPtr readPipe, out IntPtr writePipe, ref saPipeAttributes, 0))
            {
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            // 确保管道句柄有效
            if (readPipe == IntPtr.Zero)
            {
                throw new InvalidOperationException("Failed to create read pipe.");
            }
            if (writePipe == IntPtr.Zero)
            {
                throw new InvalidOperationException("Failed to create write pipe.");
            }

            try
            {
                // 确保读取句柄不被子进程继承
                SetHandleInformation(readPipe, 0x00000001/*HANDLE_FLAG_INHERIT*/, 0);

                var startInfo = new StartupInfo();
                startInfo.cb = Marshal.SizeOf(startInfo);
                // 设置子进程的标准输出为管道的写入端
                startInfo.hStdError = writePipe;
                startInfo.hStdOutput = writePipe;
                startInfo.dwFlags = StartupInfoFlags.STARTF_USESTDHANDLES;

                // 在用户会话中创建进程
                const CreateProcessFlags createProcessFlags = CreateProcessFlags.CREATE_NEW_CONSOLE | CreateProcessFlags.CREATE_UNICODE_ENVIRONMENT;
                var success = CreateProcessAsUser(
                    userToken,
                    null,
                    commandLine,
                    ref saPipeAttributes,
                    ref saPipeAttributes,
                    true,
                    createProcessFlags,
                    environmentBlock,
                    null,
                    ref startInfo,
                    out ProcessInformation pi);
                if (!success)
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                }

                // 关闭管道的写入端句柄,因为它已经被子进程继承
                CloseHandle(writePipe);
                writePipe = IntPtr.Zero;

                // 从管道的读取端读取数据
                string output;
                using (var streamReader = new StreamReader(new FileStream(new SafeFileHandle(readPipe, true), FileAccess.Read, 4096, false), encoding))
                {
                    // 读取控制台标准输出内容
                    output = streamReader.ReadToEnd();
                    Trace.WriteLine($"The commandLine [{commandLine}] std output -> {output}");
                }

                // 关闭进程和线程句柄
                CloseHandle(pi.hProcess);
                CloseHandle(pi.hThread);

                // 返回控制台标准输出内容
                return output;
            }
            finally
            {
                if (readPipe != IntPtr.Zero) CloseHandle(readPipe);
                if (writePipe != IntPtr.Zero) CloseHandle(writePipe);
            }
        }

        /// <summary>
        /// 获取活动会话的用户访问令牌
        /// </summary>
        /// <exception cref="Win32Exception"></exception>
        private static IntPtr GetSessionUserToken()
        {
            // 获取当前活动的控制台会话ID
            uint sessionId = WTSGetActiveConsoleSessionId();

            // 获取活动会话的用户访问令牌
            bool success = WTSQueryUserToken(sessionId, out IntPtr hToken);
            // 如果失败,则从会话列表中获取第一个活动的会话ID,并再次尝试获取用户访问令牌
            if (!success)
            {
                sessionId = GetFirstActiveSessionOfEnumerateSessions();
                success = WTSQueryUserToken(sessionId, out hToken);
                if (!success)
                    throw new Win32Exception(Marshal.GetLastWin32Error());
            }

            return hToken;
        }

        /// <summary>
        /// 枚举所有用户会话,获取第一个活动的会话ID
        /// </summary>
        private static uint GetFirstActiveSessionOfEnumerateSessions()
        {
            IntPtr pSessionInfo = IntPtr.Zero;
            try
            {
                int sessionCount = 0;

                // 枚举所有用户会话
                if (WTSEnumerateSessions(IntPtr.Zero, 0, 1, ref pSessionInfo, ref sessionCount) != 0)
                {
                    int arrayElementSize = Marshal.SizeOf(typeof(WtsSessionInfo));
                    IntPtr current = pSessionInfo;

                    for (int i = 0; i < sessionCount; i++)
                    {
                        WtsSessionInfo si = (WtsSessionInfo)Marshal.PtrToStructure(current, typeof(WtsSessionInfo));
                        current += arrayElementSize;

                        if (si.State == WtsConnectStateClass.WTSActive)
                        {
                            return si.SessionID;
                        }
                    }
                }

                return uint.MaxValue;
            }
            finally
            {
                WTSFreeMemory(pSessionInfo);
                CloseHandle(pSessionInfo);
            }
        }

        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, ref SecurityAttributes lpProcessAttributes, ref SecurityAttributes lpThreadAttributes, bool bInheritHandles, CreateProcessFlags dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref StartupInfo lpStartupInfo, out ProcessInformation lpProcessInformation);

        /// <summary>
        /// 以指定用户的身份启动进程
        /// </summary>
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern bool CreateProcessAsUser(
            IntPtr hToken,
            string lpApplicationName,
            string lpCommandLine,
            ref SecurityAttributes lpProcessAttributes,
            ref SecurityAttributes lpThreadAttributes,
            bool bInheritHandles,
            CreateProcessFlags dwCreationFlags,
            IntPtr lpEnvironment,
            string lpCurrentDirectory,
            ref ProcessStartInfo lpStartupInfo,
            out ProcessInformation lpProcessInformation);

        /// <summary>
        /// 获取当前活动的控制台会话ID
        /// </summary>
        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern uint WTSGetActiveConsoleSessionId();

        /// <summary>
        /// 枚举所有用户会话
        /// </summary>
        [DllImport("wtsapi32.dll", SetLastError = true)]
        private static extern int WTSEnumerateSessions(IntPtr hServer, int reserved, int version, ref IntPtr ppSessionInfo, ref int pCount);

        /// <summary>
        /// 获取活动会话的用户访问令牌
        /// </summary>
        [DllImport("wtsapi32.dll", SetLastError = true)]
        private static extern bool WTSQueryUserToken(uint sessionId, out IntPtr phToken);

        /// <summary>
        /// 复制访问令牌
        /// </summary>
        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, ref SecurityAttributes lpTokenAttributes, SecurityImpersonationLevel impersonationLevel, TokenType tokenType, out IntPtr phNewToken);

        /// <summary>
        /// 创建环境块(检索指定用户的环境)
        /// </summary>
        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit);

        /// <summary>
        /// 释放环境块
        /// </summary>
        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CreatePipe(out IntPtr hReadPipe, out IntPtr hWritePipe, ref SecurityAttributes lpPipeAttributes, uint nSize);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool SetHandleInformation(IntPtr hObject, uint dwMask, uint dwFlags);

        [DllImport("wtsapi32.dll", SetLastError = false)]
        private static extern void WTSFreeMemory(IntPtr memory);

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

        [StructLayout(LayoutKind.Sequential)]
        private struct WtsSessionInfo
        {
            public readonly uint SessionID;

            [MarshalAs(UnmanagedType.LPStr)]
            public readonly string pWinStationName;

            public readonly WtsConnectStateClass State;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct SecurityAttributes
        {
            public int Length;
            public IntPtr SecurityDescriptor;
            public bool InheritHandle;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct StartupInfo
        {
            public int cb;
            public string lpReserved;
            public string lpDesktop;
            public string lpTitle;
            public uint dwX;
            public uint dwY;
            public uint dwXSize;
            public uint dwYSize;
            public uint dwXCountChars;
            public uint dwYCountChars;
            public uint dwFillAttribute;
            public StartupInfoFlags dwFlags;
            public UInt16 wShowWindow;
            public UInt16 cbReserved2;
            public unsafe byte* lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct ProcessInformation
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public int dwProcessId;
            public int dwThreadId;
        }

        private const uint TOKEN_DUPLICATE = 0x0002;
        private const uint MAXIMUM_ALLOWED = 0x2000000;

        /// <summary>
        /// Process Creation Flags。<br/>
        /// More:https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
        /// </summary>
        [Flags]
        private enum CreateProcessFlags : uint
        {
            DEBUG_PROCESS = 0x00000001,
            DEBUG_ONLY_THIS_PROCESS = 0x00000002,
            CREATE_SUSPENDED = 0x00000004,
            DETACHED_PROCESS = 0x00000008,
            /// <summary>
            /// The new process has a new console, instead of inheriting its parent's console (the default). For more information, see Creation of a Console. <br />
            /// This flag cannot be used with <see cref="DETACHED_PROCESS"/>.
            /// </summary>
            CREATE_NEW_CONSOLE = 0x00000010,
            NORMAL_PRIORITY_CLASS = 0x00000020,
            IDLE_PRIORITY_CLASS = 0x00000040,
            HIGH_PRIORITY_CLASS = 0x00000080,
            REALTIME_PRIORITY_CLASS = 0x00000100,
            CREATE_NEW_PROCESS_GROUP = 0x00000200,
            /// <summary>
            /// If this flag is set, the environment block pointed to by lpEnvironment uses Unicode characters. Otherwise, the environment block uses ANSI characters.
            /// </summary>
            CREATE_UNICODE_ENVIRONMENT = 0x00000400,
            CREATE_SEPARATE_WOW_VDM = 0x00000800,
            CREATE_SHARED_WOW_VDM = 0x00001000,
            CREATE_FORCEDOS = 0x00002000,
            BELOW_NORMAL_PRIORITY_CLASS = 0x00004000,
            ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000,
            INHERIT_PARENT_AFFINITY = 0x00010000,
            INHERIT_CALLER_PRIORITY = 0x00020000,
            CREATE_PROTECTED_PROCESS = 0x00040000,
            EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
            PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000,
            PROCESS_MODE_BACKGROUND_END = 0x00200000,
            CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
            CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
            CREATE_DEFAULT_ERROR_MODE = 0x04000000,
            /// <summary>
            /// The process is a console application that is being run without a console window. Therefore, the console handle for the application is not set. <br />
            /// This flag is ignored if the application is not a console application, or if it is used with either <see cref="CREATE_NEW_CONSOLE"/> or <see cref="DETACHED_PROCESS"/>.
            /// </summary>
            CREATE_NO_WINDOW = 0x08000000,
            PROFILE_USER = 0x10000000,
            PROFILE_KERNEL = 0x20000000,
            PROFILE_SERVER = 0x40000000,
            CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000,
        }

        /// <summary>
        /// 指定创建进程时的窗口工作站、桌面、标准句柄和main窗口的外观。<br/>
        /// More:https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
        /// </summary>
        [Flags]
        private enum StartupInfoFlags : uint
        {
            /// <summary>
            /// 强制反馈光标显示,即使用户没有启用。
            /// </summary>
            STARTF_FORCEONFEEDBACK = 0x00000040,
            /// <summary>
            /// 强制反馈光标不显示,即使用户启用了它。
            /// </summary>
            STARTF_FORCEOFFFEEDBACK = 0x00000080,
            /// <summary>
            /// 防止应用程序被固定在任务栏或开始菜单。
            /// </summary>
            STARTF_PREVENTPINNING = 0x00002000,
            /// <summary>
            /// 不再支持,原用于强制控制台应用程序全屏运行。
            /// </summary>
            STARTF_RUNFULLSCREEN = 0x00000020,
            /// <summary>
            /// lpTitle成员是一个AppUserModelID。
            /// </summary>
            STARTF_TITLEISAPPID = 0x00001000,
            /// <summary>
            /// lpTitle成员是一个链接名。
            /// </summary>
            STARTF_TITLEISLINKNAME = 0x00000800,
            /// <summary>
            /// 启动程序来自不受信任的源,可能会显示警告。
            /// </summary>
            STARTF_UNTRUSTEDSOURCE = 0x00008000,
            /// <summary>
            /// 使用dwXCountChars和dwYCountChars成员。
            /// </summary>
            STARTF_USECOUNTCHARS = 0x00000008,
            /// <summary>
            /// 使用dwFillAttribute成员。
            /// </summary>
            STARTF_USEFILLATTRIBUTE = 0x00000010,
            /// <summary>
            /// 使用hStdInput成员指定热键。
            /// </summary>
            STARTF_USEHOTKEY = 0x00000200,
            /// <summary>
            /// 使用dwX和dwY成员。
            /// </summary>
            STARTF_USEPOSITION = 0x00000004,
            /// <summary>
            /// 使用wShowWindow成员。
            /// </summary>
            STARTF_USESHOWWINDOW = 0x00000001,
            /// <summary>
            /// 使用dwXSize和dwYSize成员。
            /// </summary>
            STARTF_USESIZE = 0x00000002,
            /// <summary>
            /// 使用hStdInput、hStdOutput和hStdError成员。
            /// </summary>
            STARTF_USESTDHANDLES = 0x00000100
        }

        private enum WtsConnectStateClass
        {
            WTSActive,
            WTSConnected,
            WTSConnectQuery,
            WTSShadow,
            WTSDisconnected,
            WTSIdle,
            WTSListen,
            WTSReset,
            WTSDown,
            WTSInit
        }

        private enum SecurityImpersonationLevel
        {
            SecurityAnonymous,
            SecurityIdentification,
            SecurityImpersonation,
            SecurityDelegation
        }

        private enum TokenType
        {
            TokenPrimary = 1,
            TokenImpersonation
        }
    }
}

用法:

ProcessUtils.StartProcessAsActiveUser("ping.exe", "www.baidu.com -t");
ProcessUtils.StartProcessAsActiveUser("notepad.exe");
ProcessUtils.StartProcessAsActiveUser("C:\\Windows\\System32\\notepad.exe");

Windows 7~11Windows Server 2016~2022 操作系统,测试通过。

PS: 2024-06-23 修改

  1. 之前的代码还有些问题,如果传入的是一个没有路径的命令,且workDir参数不为空时,则会被拼接,导致启动时找不到这个命令。
  2. 增加检查传入的命令在指定的用户会话环境中是否存在,如果不存在,则抛出异常。