在System身份运行的.NET程序中以指定的用户身份启动可交互式进程

system,net · 浏览次数 : 0

小编点评

本文介绍了如何在以System身份运行的.NET程序(Windows Services)中以其他活动的用户身份启动可交互式进程。文章首先提到了一种在GitLab流水线中实现此功能的方法,然后详细阐述了如何在C#中实现这一功能。最后,文章给出了使用示例和操作系统兼容性说明。 1. **功能介绍**: - 文章首先介绍了在Windows Services中以其他活动的用户身份启动可交互式进程的需求。 - 提到了在GitLab流水线中运行带有UI的自动化测试程序的先例。 2. **实现方法**: - 详细阐述了如何在C#中以活动用户的身份启动进程。 - 提供了包括获取当前活动会话的用户访问令牌、创建环境块、启动进程等关键步骤的代码示例。 - 强调了代码中的注意事项,如异常处理和资源清理。 3. **使用示例**: - 提供了三个具体的使用示例,展示了如何在不同情况下启动不同的进程。 - 示例包括了使用ping.exe、notepad.exe和Windows系统自带记事本程序作为进程的例子。 4. **兼容性说明**: - 文章最后指出了在Windows 7至11、Windows Server 2016至2022操作系统上的测试情况。 总的来说,文章提供了一种在.NET Windows Services中以其他活动的用户身份启动可交互式进程的有效方法,该方法可在多种Windows操作系统上顺利运行。

正文

今天在技术群里,石头哥向大家提了个问题:"如何在一个以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.Runtime.InteropServices;
using System.Runtime.Versioning;
​
namespace AllenCai.Windows
{
    /// <summary>
    /// 进程工具类
    /// </summary>
    [SupportedOSPlatform("windows")]
    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
            {
                String file = fileName;
                bool shell = string.IsNullOrEmpty(workDir) && (!fileName.Contains('/') && !fileName.Contains('\\'));
                if (shell)
                {
                    if (string.IsNullOrWhiteSpace(workDir)) workDir = Environment.CurrentDirectory;
                }
                else
                {
                    if (!Path.IsPathRooted(fileName))
                    {
                        file = !string.IsNullOrEmpty(workDir) ? Path.Combine(workDir, fileName).GetFullPath() : fileName.GetFullPath();
                    }
                    if (string.IsNullOrWhiteSpace(workDir)) workDir = Path.GetDirectoryName(file);
                }
​
                if (string.IsNullOrWhiteSpace(commandLine)) commandLine = "";
​
                // 复制令牌
                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.");
​
                // 启动信息
                ProcessStartInfo psi = new ProcessStartInfo
                {
                    UseShellExecute = shell,
                    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>
        /// 获取活动会话的用户访问令牌
        /// </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
            {
                Int32 sessionCount = 0;
​
                // 枚举所有用户会话
                if (WTSEnumerateSessions(IntPtr.Zero, 0, 1, ref pSessionInfo, ref sessionCount) != 0)
                {
                    Int32 arrayElementSize = Marshal.SizeOf(typeof(WtsSessionInfo));
                    IntPtr current = pSessionInfo;
​
                    for (Int32 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);
            }
        }
​
        /// <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("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 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;
        private const uint STARTF_USESHOWWINDOW = 0x00000001;
​
        /// <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,
        }
​
        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 操作系统,测试通过。

与在System身份运行的.NET程序中以指定的用户身份启动可交互式进程相似的内容:

在System身份运行的.NET程序中以指定的用户身份启动可交互式进程

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

Python:对程序做性能分析及计时统计

如果只是想简单地对整个程序做计算统计,通常使用UNIX下的time命令就足够了。由于我用的是Mac系统,和Linux系统的输出可能有不同,不过关键都是这三个时间:user: 运行用户态代码所花费的时间,也即CPU实际用于执行该进程的时间,其他进程和进程阻塞的时间不计入此数字;system: 在内核中执行系统调用(如I/O调用)所花费的CPU时间。total(Linux下应该是real):即挂钟时间

基于 JuiceFS 构建高校 AI 存储方案:高并发、系统稳定、运维简单

中山大学的 iSEE 实验室(Intelligence Science and System) Lab)在进行深度学习任务时,需要处理大量小文件读取。在高并发读写场景下,原先使用的 NFS 性能较低,常在高峰期导致数据节点卡死。此外,NFS 系统的单点故障问题也导致一旦数据节点宕机,该机器上的数据将

[转帖]exportfs命令

https://www.cnblogs.com/xzlive/p/9766388.html exportfs命令:功能说明 :NFS共享管理语法格式exportfs [必要参数][选择参数][目录]功能描述exportfs 命令:用于管理NFS(Network File System)文件系统,在不

在 .NET 7上使用 WASM 和 WASI

WebAssembly(WASM)和WebAssembly System Interface(WASI)为开发人员开辟了新的世界。.NET 开发人员在 Blazor WebAssembly 发布时熟悉了 WASM。Blazor WebAssembly 在浏览器中基于 WebAssembly 的 .N

Hyperledger Fabric系统链码介绍

在Hyperledger Fabric中,LSCC(Lifecycle System Chaincode)、CSCC(Chaincode System Chaincode)、QSCC(Query System Chaincode)、ESCC(Endorsement System Chaincode)

ADVMP 三代壳(vmp加固)原理分析(执行流程)

由于在加壳时插入了System.loadLibrary("advmp");,看一下JNI_OnLoad JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL; if (vm->GetEnv

关于.Net 6.0 在Linux ,Docker容器中,不安装任何依赖就生成图形验证码!!!!!!!!!!!

在.Net Framework时代,我们生成验证码大多都是用System.Drawing。 在.Net 6中使用也是没有问题的。 但是,System.Drawing却依赖于Windows GDI+。 为了实现跨平台,我陷入了沉思!! 微软推荐使用SkiaSharp 进行替代,所以就开始了,踩坑之旅

【转帖】linux 内核分析工具 Dtrace、SystemTap、火焰图、crash等

<< System语言详解 >> 关于 SystemTap 的书。 我们在分析各种系统异常和故障的时候,通常会用到 pstack(jstack) /pldd/ lsof/ tcpdump/ gdb(jdb)/ netstat/vmstat/ mpstat/truss(strace)/iostat/s

.NET使用原生方法实现文件压缩和解压

前言 在.NET中实现文件或文件目录压缩和解压可以通过多种方式来完成,包括使用原生方法(System.IO.Compression命名空间中的类)和第三方库(如:SharpZipLib、SharpCompress、K4os.Compression.LZ4等)。本文我们主要讲的是如何使用.NET原生方