Dajbych.net


Double layered application with PowerShell in the middle

, 6 minutes to read

powershell logo

There are several reasons to use PowerShell. The most significant advantage is the ability to use scripts instead of the user interface. For administrators, PowerShell is almost the same environment as a console application is for developers. When the core of your application is highly independent, it is extremely easy to build a console application based on it, so consider using a PowerShell layer.

Why should I use a PowerShell interface in my application instead of directly referencing the library? When a developer wants to explore a library, the first step is usually a console application. It is the developer’s sandbox with quick graphical output because every object has a ToString method. Not everyone knows how to write a console application. Administrators like PowerShell because the write-compile-run loop is much faster. Developers like code where a breakpoint can be set on every command. So again, why a PowerShell interface? The answer is not surprising – because it is very useful for a specific kind of application.

When the code configures something, it is clear that implementing PowerShell support is a great investment. In my case, I used a PowerShell interface for a cryptanalysis library. There are many algorithms, but I didn’t want to hardcode the steps to decrypt an encrypted message. I wanted to give the user the ability to use algorithms and decide based on heuristics and their own judgment which ones to put together. It is a similar approach to workflows defined in XAML. What PowerShell can do best is fast user interaction, and of course, IntelliSense is not lost because PowerShell has autocomplete capability.

The architecture is simple. The library implements a PowerShell interface. The user interface runs a PowerShell host, generating commands to it and receiving objects from it to fill ViewModels. Implementing this pattern is not trivial for many reasons. First of all, there are basically two versions of .NET – 2.0 and 4.0 (1.0 is obsolete, and 3.0 and 3.5 are extensions of 2.0). PowerShell has two versions, with a third currently in beta. Another complication is the dual environment – 32-bit and 64-bit. I spent many hours solving issues caused by these aspects. When I was finally done, I felt it was my duty to write an article about it.

Let’s talk about the library’s point of view first. The library has to reference the System.Management.Automation library, which is part of the PowerShell SDK. There are two important classes – PSCmdlet and CustomPSSnapIn. PSCmdlet is a command that PowerShell can execute and optionally return an object or collection. CustomPSSnapIn is a set of PowerShell commands with vendor information in one package. Your classes must derive from them. Cmdlet classes must be decorated with the Cmdlet attribute, and the SnapIn class must be decorated with the RunInstaller attribute. This is required for reflection because that’s how PowerShell finds cmdlets in your library. The library is compiled to MSIL, so when you keep the Any CPU configuration, it works in both 32-bit and 64-bit environments. You should choose .NET Framework 3.5 for PowerShell 2 and .NET Framework 4 for PowerShell 3. A SnapIn assembly compiled as .NET 3.5 should work fine in PowerShell 3, but not vice versa.

The library containing cmdlets must be registered. There is an InstallUtil.exe that does this job for you. The problem is this utility is platform-dependent. Avoiding guessing which framework version to run or whether to use the 32-bit or 64-bit variant requires knowing that this utility is just a wrapper of the AssemblyInstaller class. This means that during application launch, the library can be easily registered so the hosting PowerShell knows about its existence. During application exit, the library is unregistered so its location can be changed in the future without causing any errors. This registration approach doesn’t fit all scenarios. Library registration should be done by the installer rather than the application itself, but who likes installers? Users who use PowerShell often have User Account Protection turned off because they know what they are doing.

/* LIBRARY IMPLEMENTING POWERSHELL CMDLETS SAMPLE */

[Cmdlet(VerbsCommunications.Send, "MyCmdlet")]
public class GetMyCmdletCommand : PSCmdlet {

    [Parameter(Mandatory = true, HelpMessage = "This is a sample parameter.")]
    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<CmdletConfigurationEntry>();
        cmdlets.Add(new CmdletConfigurationEntry("Get-MyCmdlet", typeof(GetMyCmdletCommand), null));
    }

    public override string Description {
        get { return "Demo"; }
    }

    public override string Name {
        get { return "SnapInName"; }
    }

    public override string Vendor {
        get { return "Václav Dajbych"; }
    }

    private Collection<CmdletConfigurationEntry> cmdlets;
    public override Collection<CmdletConfigurationEntry> Cmdlets {
        get {
            return cmdlets;
        }
    }
}

The user interface point of view is more difficult. First of all, it is worth knowing that even a 64-bit application runs a 32-bit PowerShell instance. The application’s .NET Framework version is not important. What is important is the ability to reference the System.Management.Automation library. This library is located in the v1.0 directory, but it doesn’t mean that PowerShell 1 is used. The latest available version is always used.

When the user interface launches a PowerShell host, it must load the SnapIn first using the AddPSSnapIn class in RunspaceConfiguration. Then the application can call the PowerShell.Create method and use PowerShell commands defined in the library. The advantage of PowerShell 3 is that it is built on top of the *Dynamic Language Runtime. This means you can type PSObject as dynamic and shorten your code.

/* USER INTERFACE INTERACTION SAMPLE */
  
// PowerShell host
Runspace runSpace;
  
private Init() {
  
  // load PowerShell
  var rsConfig = RunspaceConfiguration.Create();
  runSpace = RunspaceFactory.CreateRunspace(rsConfig);
  runSpace.Open();

  // register 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);
  }

  // load 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;
    }
}

Hope this helps in building friendly configured environments. Once you know how to do that, you will find it’s really easy. Just follow the naming conventions.