今天在技术群里,石头哥向大家提了个问题:"如何在一个以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程序中以指定的用户身份启动可交互式进程的更多相关文章

  1. 在DevExpress程序中使用SplashScreenManager控件实现启动闪屏和等待信息窗口

    在我很早的WInform随笔<WinForm界面开发之"SplashScreen控件">有介绍如何使用闪屏的处理操作,不过那种是普通WInform和DevExpress ...

  2. 在SharePoint 2013中显示“以其他用户身份登录”

    在我新建了SharePoint 2013的网站后, 发现界面与2010有一些不同,比如缺少了“以其他用户身份登录”,这给我的测试带来很大不便. 在找了一些国外网站后,终于找到了解决方法 第一步: 找到 ...

  3. (PHP)程序中如何判断当前用户终端是手机等移动终端

    推荐: Mobile-Detect:https://github.com/serbanghita/Mobile-Detect/blob/master/Mobile_Detect.php Detect ...

  4. Rhino 是一个完全使用Java语言编写的开源JavaScript实现。Rhino通常用于在Java程序中,为最终用户提供脚本化能力。它被作为J2SE 6上的默认Java脚本化引擎。

    https://developer.mozilla.org/zh-CN/docs/Mozilla/Projects/Rhino

  5. 在ASP.NET应用程序中使用身份模拟(Impersonation)

    摘要   缺省情况下,ASP.NET应用程序以本机的ASPNET帐号运行,该帐号属于普通用户组,权限受到一定的限制,以保障ASP.NET应用程序运行的安全.但是有时需要某个ASP.NET应用程序或者程 ...

  6. C#以管理员身份运行程序

    using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; names ...

  7. Runas命令:能让域用户/普通User用户以管理员身份运行指定程序。

    注:本文由Colin撰写,版权所有!转载请注明原文地址,谢谢合作! 在某些情况下,为了安全起见,大部分公司都会使用域控制器或只会给员工电脑user的用户权限,这样做能大大提高安全性和可控性,但由此也带 ...

  8. 【CITE】C#默认以管理员身份运行程序实现代码

    //用于一种情况:C#软件打包后,在读写C盘文件时,会出现权限问题.使用管理员身份才可以运行 using System; using System.Collections.Generic; using ...

  9. SUID或SGID程序中能不能用system函数

    system()函数的声明和说明如下: 注意它的描述那里,system()执行一个由command参数定义的命令,通过调用/bin/sh -c命令来实现这个功能.也就是说它的逻辑是这样的! 进程调用s ...

  10. C#编写以管理员身份运行的程序

    using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; names ...

随机推荐

  1. 基于 MaxCompute + Hologres 的人群圈选和数据服务实践

    ​简介: 本文主要介绍如何通过 MaxCompute 进行海量人群的标签加工,通过 Hologres 进行分析建模,从而支持大规模人群复杂圈选场景下的交互式体验,以及基于API的数据服务最佳实践. 本 ...

  2. [FAQ] FinalCutPro 事件如何支持多个时间线

    左侧是建立的事件,右侧是默认的项目(也就是时间线上的剪辑项目). 如果需要这个事件里再弄一个时间线(比如剪辑另一个版本),左侧的事件上右击新建项目: 另一个项目,在这上面可以继续时间线的创作,等于是选 ...

  3. [FAQ] golang-migrate/migrate error: migration failed in line 0: (details: Error 1065: Query was empty)

    当我们使用 migrate create 创建了迁移文件. 没有及时填写内容,此时运行 migrate 的后续命令比如 up.down 会抛出错误: error: migration failed i ...

  4. [PHP] 浅谈 Laravel Authorization 的 gates 与 policies

    首先要区分 Authentication 与 Authorization,认证和授权,粗细有别. 授权(Authorization) 有两种主要方式,Gates 和 Policies. Gates 和 ...

  5. 21.3K star!推荐一款可视化自动化测试/爬虫/数据采集神器!功能免费且强大!

    大家好,我是狂师! 在大数据时代,信息的获取与分析变得尤为重要.对于开发者.数据分析师乃至非技术人员来说,能够高效地采集网络数据并进行分析是一个强有力的工具.今天,我要向大家推荐的是一款功能强大.操作 ...

  6. K8s控制器---Replicaset(7)

    一.Replicaset(目前少用了) 1.1 控制器管理pod 什么是控制器?前面我们学习了 Pod,那我们在定义 pod 资源时,可以直接创建一个 kind:Pod 类型的自主式 pod,但是这存 ...

  7. VUE+element页面按钮调用dialog

    VUE+element通过按钮调用普通弹框(弹框页面独立出一个dialog页面,非在同一个页面文件里) 代码如下 <el-dialog> <el-button type=" ...

  8. SuperSonic简介

    SuperSonic融合ChatBI和HeadlessBI打造新一代的数据分析平台.通过SuperSonic的问答对话界面,用户能够使用自然语言查询数据,系统会选择合适的可视化图表呈现结果. Supe ...

  9. 《最新出炉》系列入门篇-Python+Playwright自动化测试-44-鼠标操作-上篇

    1.简介 前边文章中已经讲解过鼠标的拖拽操作,今天宏哥在这里对其的其他操作进行一个详细地介绍和讲解,然后对其中的一些比较常见的.重要的操作单独拿出来进行详细的介绍和讲解. 2.鼠标操作语法 鼠标操作介 ...

  10. go 操作 Excel

    文档地址: https://xuri.me/excelize/zh-hans/ package main import ( "fmt" "github.com/xuri/ ...