今天在技术群里,石头哥向大家提了个问题:"如何在一个以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(){// 获取当前活动的控制台会话IDuint 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~11
、Windows Server 2016~2022
操作系统,测试通过。