PowerShell是一种强大的命令行工具,它为管理员和开发者提供了一个几乎相同的环境。当应用程序的核心是高度独立的,那么基于它构建一个控制台应用程序是极其容易的,考虑使用 PowerShell 层。
为什么要在应用程序中使用 PowerShell 接口而不是直接引用库?当开发者想要探索一个库时,通常的第一步是创建一个控制台应用程序。它是开发者的沙箱,可以快速地以图形方式输出,因为每个对象都有一个 ToString 方法。并非每个人都知道如何编写控制台应用程序。管理员喜欢 PowerShell,因为编写-编译-运行循环要快得多。开发者喜欢代码,因为可以在每个命令上设置断点。那么,为什么选择 PowerShell 接口呢?答案并不令人惊讶——因为它对于特定类型的应用程序非常有用。
当代码配置某些东西时,很明显实现 PowerShell 支持是一项很好的投资。在例子中,为一个密码分析库使用了一个 PowerShell 接口。有许多算法,但不想硬编码解密加密消息的步骤。想让用户能够使用算法,并根据启发式和自己的判断决定哪些算法组合在一起。这类似于在 XAML 中定义的工作流。PowerShell 最擅长的是快速用户交互,当然,由于 PowerShell 具有自动完成功能,IntelliSense 也不会丢失。
架构很简单。库实现了 PowerShell 接口。用户界面运行一个 PowerShell 主机,生成命令并接收对象以填充视图模型。实现这种模式并不简单,原因有很多。首先,基本上有两个版本的 .NET——2.0 和 4.0(1.0 已经过时,3.0 和 3.5 是 2.0 的扩展)。PowerShell 有两个版本,第三个目前处于 beta 阶段。另一个复杂性是双环境——32 位和 64 位。花了很长时间解决这些问题。当最终完成后,觉得有责任写一篇文章。
库必须引用 System.Management.Automation 库,这是PowerShellSDK 的一部分。有两个重要的类——PSCmdlet 和 CustomPSSnapIn。PSCmdlet 是 PowerShell 可以执行的命令,可以选择性地返回一个对象或集合。CustomPSSnapIn 是一组 PowerShell 命令,包含在一个包中的供应商信息。类必须从它们派生。Cmdlet 类必须用 Cmdlet 属性装饰,SnapIn 类必须用 RunInstaller 属性装饰。这是反射所必需的,因为这就是 PowerShell 在库中找到 cmdlets 的方式。库被编译成 MSIL,所以当保持 Any CPU 配置时,它可以在 32 位和 64 位环境中工作。应该选择 .NET Framework 3.5 对应 PowerShell 2,.NET Framework 4 对应 PowerShell 3。编译为 .NET 3.5 的 SnapIn 程序集应该可以在 PowerShell 3 中正常工作,但反之则不行。
包含 cmdlets 的库必须注册。有一个 InstallUtil.exe 工具可以完成这项工作。问题是这个工具是平台依赖的。避免猜测哪个框架版本正在运行,或者是否使用 32 位或 64 位变体,需要知道这个工具只是一个 AssemblyInstaller 类的包装器。这意味着在应用程序启动期间,库可以轻松注册,因此托管 PowerShell 知道它的存在。在应用程序退出时,库被注销,所以它的位置可以在将来更改,而不会引起任何错误。这种注册方法并不适合所有场景。库的注册应该由安装程序而不是应用程序本身来完成,但是谁喜欢安装程序呢?使用 PowerShell 的用户通常关闭用户帐户保护,因为他们非常清楚自己在做什么。
[Cmdlet(VerbsCommunications.Send, "MyCmdlet")]
public class GetMyCmdletCommand : PSCmdlet {
[Parameter(Mandatory = true, HelpMessage = "这是一个示例参数。")]
public string Param { get; set; }
protected override void ProcessRecord() {
var obj = new ObjectToReturn(Param);
WriteObject(obj);
}
}
[RunInstaller(true)]
public class Cryptanalysis : CustomPSSnapIn {
public Cryptanalysis() {
cmdlets = new Collection();
cmdlets.Add(new CmdletConfigurationEntry("Get-MyCmdlet", typeof(GetMyCmdletCommand), null));
}
public override string Description {
get { return "演示"; }
}
public override string Name {
get { return "SnapInName"; }
}
public override string Vendor {
get { return "Václav Dajbych"; }
}
private Collection cmdlets;
public override Collection Cmdlets {
get { return cmdlets; }
}
}
用户界面的角度更困难。首先值得知道的是,即使是 64 位应用程序也会运行 32 位 PowerShell 实例。应用程序的 .NET Framework 版本并不重要。重要的是能够引用 System.Management.Automation 库。这个库位于 v1.0 目录中,但这并不意味着使用了 PowerShell 1。总是使用最新可用的版本。
// PowerShell 主机
Runspace runSpace;
private Init() {
// 加载 PowerShell
var rsConfig = RunspaceConfiguration.Create();
runSpace = RunspaceFactory.CreateRunspace(rsConfig);
runSpace.Open();
// 注册 snapin
using (var ps = PowerShell.Create()) {
ps.Runspace = runSpace;
ps.AddCommand("Get-PSSnapin");
ps.AddParameter("Registered");
ps.AddParameter("Name", "SnapInName");
var result = ps.Invoke();
if (result.Count == 0) Register(false);
}
// 加载 snapin
PSSnapInException ex;
runSpace.RunspaceConfiguration.AddPSSnapIn("SnapInName", out ex);
}
void Register(bool undo) {
var core = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "MySnapInLib.dll");
using (var install = new AssemblyInstaller(core, null)) {
IDictionary state = new Hashtable();
install.UseNewContext = true;
try {
if (undo) {
install.Uninstall(state);
} else {
install.Install(state);
install.Commit(state);
}
} catch {
install.Rollback(state);
}
}
}
dynamic DoJob(string parameter) {
using (var ps = PowerShell.Create()) {
ps.Runspace = runSpace;
ps.AddCommand("Get-MyCmdlet");
ps.AddParameter("Param", parameter);
dynamic result = ps.Invoke().Single();
return result.ReturnedObjectProperty;
}
}