今天在技術群里,石頭哥向大家提了個問題:"如何在一個以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 編輯過