☰
使用 CommandMetadata 与 ProxyCommand 构建受限 Runspace:Windows PowerShell Runspace11 示例深度解析
2026/10/16 6:09:54 网站建设 项目流程
  • 示例工程

【免费下载链接】Windows-classic-samples

This repo contains samples that demonstrate the API used in Windows classic desktop applications.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-classic-samples
点击查看免费下载

本文围绕 Windows-classic-samples 仓库中的 Runspace11 示例,完整讲解如何通过CommandMetadata描述既有 cmdlet 的元数据、移除其中部分参数,再用ProxyCommand.Create生成代理函数(proxy function),最终放入InitialSessionState构建一个受限 runspace(constrained runspace)。读完本文,你将掌握"让用户只能通过受控代理间接调用 cmdlet、且仅能使用允许的参数子集"这一经典约束模式,并能直接对照源码复现运行。

示例要解决什么问题

在托管 Windows PowerShell 引擎的宿主应用中,默认InitialSessionState.CreateDefault()创建的 runspace 会暴露引擎提供的全部命令与语言能力。对于面向普通用户或安全敏感场景的宿主,需要将命令集与参数集收窄。Runspace11 正是这一场景的最小可运行示范:

  • 定义一个名为Get-Proc的自定义 cmdlet(实现取自 GetProcessSample02);
  • 通过SessionStateCmdletEntry把它加入初始会话状态,并将可见性设为Private;
  • 基于该 cmdlet 的类型构造CommandMetadata,从元数据中移除Name参数;
  • 调用ProxyCommand.Create生成一段代理函数脚本,代理内部仍调用原始 cmdlet,但对外只暴露允许的参数;
  • 把代理函数以新名称get-procProxy加入同一初始会话状态;
  • 创建受限 runspace,并依次验证:私有 cmdlet 直接调用失败、代理函数带被移除参数调用失败、代理函数正常调用成功。

整个过程对应原 README 列出的六个目标:创建CommandMetadata、修改元数据删除参数、把 cmdlet 加入会话状态并设为私有、生成受限参数的代理函数、注册代理函数、通过调用演示受限效果。

关键概念:CommandMetadata、代理命令与受限 Runspace

CommandMetadata

CommandMetadata(位于System.Management.Automation命名空间)用于描述一个命令的元数据,包括命令名、参数集以及各参数的声明信息。示例中直接以 cmdlet 类型构造:

CommandMetadata cmdletMetadata = new CommandMetadata(typeof(GetProcCommand));

此后cmdletMetadata.Parameters集合即代表该 cmdlet 对外可见的参数表。示例通过Parameters.Remove("Name")精确摘除某个参数,从而让后续生成的代理函数"看不到"该参数。

ProxyCommand 与代理函数

ProxyCommand.Create(cmdletMetadata)根据元数据生成一段 PowerShell 函数体(脚本字符串)。这段函数在内部以Microsoft.PowerShell.Core\...方式调用底层 cmdlet,同时按元数据重新声明对外参数。它相当于给原始命令包了一层"门卫":

  • 函数名可与原 cmdlet 相同,但示例刻意使用不同名称get-procProxy,以便清楚展示原始 cmdlet 已不可访问;
  • 函数只暴露元数据中保留的参数,被移除的参数在函数签名中不存在,调用时必然触发参数绑定错误。

InitialSessionState 与受限 Runspace

InitialSessionState决定创建 runspace 时的初始命令、变量、模块与会话配置。示例的核心约束手段有三处:

InitialSessionState iss = InitialSessionState.CreateDefault(); SessionStateCmdletEntry cmdletEntry = new SessionStateCmdletEntry("get-proc", typeof(GetProcCommand), null); iss.Commands.Add(cmdletEntry); cmdletEntry.Visibility = SessionStateEntryVisibility.Private; iss.LanguageMode = PSLanguageMode.NoLanguage;
  • CreateDefault():构建包含引擎全部默认元素的初始会话状态;
  • Visibility = SessionStateEntryVisibility.Private:把get-proc标记为私有命令,用户会话中无法直接解析该命令名;
  • PSLanguageMode.NoLanguage:禁用脚本语言特性,只允许调用公开命令,进一步收紧宿主能力。

随后RunspaceFactory.CreateRunspace(iss)以该会话状态创建受限 runspace,所有命令都在这套约束之下运行。

源码级剖析:受约束的 Get-Proc cmdlet

示例的 cmdlet 定义位于 Runspace11.cs 的GetProcCommand类(源码注释说明其原样取自 GetProcessSample02.cs):

[Cmdlet(VerbsCommon.Get, "Proc")] public class GetProcCommand : Cmdlet { private string[] processNames; [Parameter(Position = 0)] [ValidateNotNullOrEmpty] public string[] Name { get { return this.processNames; } set { this.processNames = value; } } protected override void ProcessRecord() { if (this.processNames == null) { WriteObject(Process.GetProcesses(), true); } else { foreach (string name in this.processNames) { WriteObject(Process.GetProcessesByName(name), true); } } } }

要点:

  • [Cmdlet(VerbsCommon.Get, "Proc")]声明命令名Get-Proc,注册到会话状态时使用小写连字符形式get-proc;
  • Name参数带[Parameter(Position = 0)],即位置参数,后续移除它即可让代理函数不再接受任何按位置或按名称传入的进程名;
  • 无参数时输出全部进程,带参数时按名称过滤输出,行为简单直观,便于验证约束前后差异。

源码级剖析:三次调用验证受限效果

受限 runspace 创建并Open()之后,Main方法依次执行三次调用(见 Runspace11.cs)。

第一次:直接调用私有 cmdlet —— 预期失败

using (PowerShell powershell = PowerShell.Create()) { powershell.Runspace = myRunspace; powershell.AddCommand("get-proc").AddParameter("Name", "*explore*"); powershell.Invoke(); }

由于get-proc的可见性为Private,命令名在受限会话中不可解析,Invoke()抛出CommandNotFoundException。示例捕获后打印:

Invoking 'get-proc' failed as expected: System.Management.Automation.CommandNotFoundException: ...

这一步证明了"用户不能直接调用原始 cmdlet"。

第二次:代理函数带被移除参数调用 —— 预期失败

powershell.AddCommand("get-procProxy").AddParameter("Name", "idle");

代理函数的参数签名中已没有Name,因此抛出ParameterBindingException:

Invoking 'get-procProxy -Name idle' failed as expected: System.Management.Automation.ParameterBindingException: ...

这一步证明了"代理函数只暴露被允许的参数子集"。

第三次:代理函数无参数调用 —— 预期成功

using (PowerShell powershell = PowerShell.Create()) { powershell.Runspace = myRunspace; powershell.AddCommand("get-procProxy"); List<Process> processes = new List<Process>(powershell.Invoke<Process>()); System.Console.WriteLine( "\nInvoking the get-procProxy function called into the get-proc cmdlet and returned {0} processes", processes.Count); }

代理函数正常执行,并真正调用到底层私有 cmdlet,返回全部进程列表。这一步同时证明了两点:代理函数与底层 cmdlet 之间的内部调用链是通的,以及受限 runspace 下用户仍可通过受控入口获得命令能力。

三次调用的结果组合起来,恰好构成对"受限 runspace"行为契约的完整验证:命令级不可见、参数级不可用、功能级可用。

完整调用链一览

为便于整体把握,将示例主流程串起来(关键语句来自 Runspace11.cs):

// 1. 默认初始会话状态 InitialSessionState iss = InitialSessionState.CreateDefault(); // 2. 注册 get-proc 并设为私有 SessionStateCmdletEntry cmdletEntry = new SessionStateCmdletEntry("get-proc", typeof(GetProcCommand), null); iss.Commands.Add(cmdletEntry); cmdletEntry.Visibility = SessionStateEntryVisibility.Private; // 3. 禁用语言特性 iss.LanguageMode = PSLanguageMode.NoLanguage; // 4. 基于 cmdlet 元数据生成代理函数 CommandMetadata cmdletMetadata = new CommandMetadata(typeof(GetProcCommand)); cmdletMetadata.Parameters.Remove("Name"); string bodyOfProxyFunction = ProxyCommand.Create(cmdletMetadata); // 5. 注册代理函数(使用不同名称以区分原始 cmdlet) iss.Commands.Add(new SessionStateFunctionEntry("get-procProxy", bodyOfProxyFunction)); // 6. 用受限会话状态创建 runspace using (Runspace myRunspace = RunspaceFactory.CreateRunspace(iss)) { myRunspace.Open(); // ... 三次验证调用(见上文) ... myRunspace.Close(); }

注意源码中的代理函数体bodyOfProxyFunction是运行时由ProxyCommand.Create生成的脚本字符串,其内容在本示例中并未显式打印;如果需要查看生成的函数体,可在调试器中读取该字符串,或在会话中执行Get-Command get-procProxy观察其签名。

构建与运行

工程结构

示例的 C# 实现位于 Samples/PowerShell/Runspace/Runspace11/cs 目录,包括:

  • Runspace11.cs:全部源码(cmdlet 定义 + Main 流程);
  • Runspace11.csproj:MSBuild 工程文件;
  • Runspace11.sln:Visual Studio 解决方案(Format Version 11.00,即 VS2012);
  • AssemblyInfo.cs:程序集元数据;
  • Readme.txt:英文构建/运行说明。

构建步骤(对应原 README 的 Build the sample)

  1. 启动 Microsoft Visual Studio,选择File > Open > Project/Solution;
  2. 进入 Runspace11 示例目录,双击cs下的Runspace11.sln打开解决方案;
  3. 按F7或使用Build > Build Solution编译。

编译产物输出到默认目录。从 Runspace11.csproj 的配置看:

  • Debug 配置输出到.\bin\Debug\,并定义DEBUG;TRACE常量;
  • Release 配置输出到.\bin\Release\,定义TRACE常量;
  • 目标框架为.NET Framework v4.0,OutputType为EXE,程序集名为Runspace11。

运行步骤(对应原 README 的 Run the sample)

  1. 打开命令提示符(Command Prompt);
  2. 切换到包含示例可执行文件的目录(如cs\bin\Debug\或cs\bin\Release\);
  3. 运行Runspace11.exe。

程序会依次输出三次调用结果(两次"as expected"的异常信息与一次进程计数),随后显示Hit any key to exit...,按任意键退出。

运行环境与依赖

原 README 声明的操作系统要求为:

  • 客户端:Windows 8.1;
  • 服务器:Windows Server 2012 R2。

工程依赖方面,Runspace11.csproj 明确引用了:

  • System.Management.Automation, Version=3.0.0.0(PublicKeyToken31bf3856ad364e35),HintPath 指向Windows\Microsoft.NET\assembly\GAC_MSIL下的 GAC 程序集——这说明构建机器上需安装对应版本的 Windows PowerShell SDK/运行时;
  • 标准引用System、System.Configuration.Install、System.Data、System.Management、System.Xml。

当前仓库的 Runspace 系列 还包含 Runspace01~Runspace10 等兄弟示例(如独立的 cmdlet、直接调用、批量调用等主题),Runspace11 的约束模式可以与它们对照阅读,理解同一引擎在不同会话状态下的能力边界。

设计要点小结

从源码结构看,Runspace11 给出了一套可复用的"最小权限代理"范式,核心结论如下:

  1. 命令隐藏靠 Visibility,而不是删除:把原始 cmdlet 设为Private保留其存在(代理函数仍可调用它),同时让用户会话无法直接解析命令名,是"内部可用、外部不可见"的关键;
  2. 参数裁剪靠 CommandMetadata:CommandMetadata.Parameters.Remove(...)在元数据层面删参数,生成的代理函数签名随之收窄,用户传入被删参数必然触发ParameterBindingException;
  3. 入口收窄靠 LanguageMode:PSLanguageMode.NoLanguage从语言层封堵脚本表达式,进一步降低被滥用的风险;
  4. 验证闭环完整:三次调用分别覆盖"命令不可见、参数不可用、功能可用"三个断言,异常类型(CommandNotFoundException、ParameterBindingException)精确对应各自失败原因,可作为编写受限宿主自测用例的模板。

该模式适合嵌入需要向不可信用户暴露有限管理命令的宿主程序,例如只允许"查看进程列表"却禁止按名称筛选/操作进程的场景。结合 Runspace11.cs 中的完整实现与注释,读者可以直接在其基础上扩展自己的 cmdlet 与参数白名单。

  • 示例工程

【免费下载链接】Windows-classic-samples

This repo contains samples that demonstrate the API used in Windows classic desktop applications.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-classic-samples
点击查看免费下载
上一篇:英雄联盟客户端自动化工具包终极指南:如何5分钟提升你的游戏体验
下一篇:阿里巴巴Dragonwell17 JDK完整指南:如何快速部署高性能Java运行环境

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询