今天在技术群里,石头哥向大家提了个问题:"如何在一个以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
{
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public static class ProcessUtils
{
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));
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;
if ((!fileName.Contains('/') && !fileName.Contains('\\')))
{
if (!string.IsNullOrEmpty(workDir))
{
if (File.Exists(Path.Combine(workDir, fileName)))
{
theCommandIsInPath = false;
}
else
{
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
{
theCommandIsInPath = true;
}
}
}
else
{
if (!InPathOfSpecificUserEnvironment(in duplicateToken, in environmentBlock, fileName))
{
throw new ApplicationException($"The file '{fileName}' was not found in the PATH environment variable.");
}
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());
}
return pi.dwProcessId;
}
finally
{
if (userToken != IntPtr.Zero) CloseHandle(userToken);
if (duplicateToken != IntPtr.Zero) CloseHandle(duplicateToken);
if (environmentBlock != IntPtr.Zero) DestroyEnvironmentBlock(environmentBlock);
}
}
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);
var comparison = Path.DirectorySeparatorChar == '\\' ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
return output.IndexOf(command, comparison) >= 0;
}
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;
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, 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);
}
}
private static IntPtr GetSessionUserToken()
{
uint sessionId = WTSGetActiveConsoleSessionId();
bool success = WTSQueryUserToken(sessionId, out IntPtr hToken);
if (!success)
{
sessionId = GetFirstActiveSessionOfEnumerateSessions();
success = WTSQueryUserToken(sessionId, out hToken);
if (!success)
throw new Win32Exception(Marshal.GetLastWin32Error());
}
return hToken;
}
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);
[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);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WTSGetActiveConsoleSessionId();
[DllImport("wtsapi32.dll", SetLastError = true)]
private static extern int WTSEnumerateSessions(IntPtr hServer, int reserved, int version, ref IntPtr ppSessionInfo, ref int pCount);
[DllImport("wtsapi32.dll", SetLastError = true)]
private static extern bool WTSQueryUserToken(uint sessionId, out IntPtr phToken);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, ref SecurityAttributes lpTokenAttributes, SecurityImpersonationLevel impersonationLevel, TokenType tokenType, out IntPtr phNewToken);
[DllImport("userenv.dll", SetLastError = true)]
private static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit);
[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;
[Flags]
private enum CreateProcessFlags : uint
{
DEBUG_PROCESS = 0x00000001,
DEBUG_ONLY_THIS_PROCESS = 0x00000002,
CREATE_SUSPENDED = 0x00000004,
DETACHED_PROCESS = 0x00000008,
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,
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,
CREATE_NO_WINDOW = 0x08000000,
PROFILE_USER = 0x10000000,
PROFILE_KERNEL = 0x20000000,
PROFILE_SERVER = 0x40000000,
CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000,
}
[Flags]
private enum StartupInfoFlags : uint
{
STARTF_FORCEONFEEDBACK = 0x00000040,
STARTF_FORCEOFFFEEDBACK = 0x00000080,
STARTF_PREVENTPINNING = 0x00002000,
STARTF_RUNFULLSCREEN = 0x00000020,
STARTF_TITLEISAPPID = 0x00001000,
STARTF_TITLEISLINKNAME = 0x00000800,
STARTF_UNTRUSTEDSOURCE = 0x00008000,
STARTF_USECOUNTCHARS = 0x00000008,
STARTF_USEFILLATTRIBUTE = 0x00000010,
STARTF_USEHOTKEY = 0x00000200,
STARTF_USEPOSITION = 0x00000004,
STARTF_USESHOWWINDOW = 0x00000001,
STARTF_USESIZE = 0x00000002,
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~11
、Windows Server 2016~2022
操作系统,测试通过。
转自https://www.cnblogs.com/VAllen/p/18257879
该文章在 2025/5/17 10:03:00 编辑过