Last active
June 16, 2022 05:36
-
-
Save rjmholt/02fe49189540acf0d2650f571f5176db to your computer and use it in GitHub Desktop.
Examples of how to run PowerShell with a runspace pool and async Tasks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Collections.ObjectModel; | |
using System.IO; | |
using System.Management.Automation; | |
using System.Management.Automation.Runspaces; | |
using System.Reflection; | |
using System.Threading.Tasks; | |
/// <summary> | |
/// Encapsulates a simple way to run PowerShell commands and scripts | |
/// through a runspace pool without needing to expose that runspace pool. | |
/// | |
/// Each instance of this class will have its own runspace pool, | |
/// and any PowerShell commands run through it will run in that runspace pool. | |
/// This guarantees a maximum footprint and parallelism level for PowerShell commands. | |
/// It also means you have a single place to decide your runspace/thread semantics, | |
/// since you can reconfigure your runspace pool easily. | |
/// | |
/// This class has a lot of methods that are just examples | |
/// of how to use it in specific cases and how to use the PowerShell/PSCommand APIs. | |
/// If you're implementing this in your own code, | |
/// consider either providing only the generic methods that accept a PSCommand | |
/// or only providing concrete methods that implement required functionality for your application. | |
/// Ideally you should do both: implement the generic task runner, | |
/// and then use it within your domain-oriented class that performs concrete operations. | |
/// </summary> | |
public class PowerShellTaskRunner | |
{ | |
public static PowerShellTaskRunner Create() | |
{ | |
// Use the ISS to set up the starting state you want | |
// for runspaces in the runspace pool | |
var iss = InitialSessionState.CreateDefault(); | |
// This call takes an array of module paths or names | |
// if you want to import multiple modules, put them into an array and call this API once | |
iss.ImportPSModule(new [] { s_pathToModuleToLoad }); | |
// Mysteriously there's no overload here that allows you to set both the ISS | |
// and the min/max runspace count without also providing a host. | |
// But the default host is the internal DefaultHost. | |
// So the best way to set the things we want without needing to provide a host is like this: | |
RunspacePool rsPool = RunspaceFactory.CreateRunspacePool(iss); | |
rsPool.SetMinRunspaces(1); | |
rsPool.SetMaxRunspaces(5); | |
return new PowerShellTaskRunner(rsPool); | |
} | |
// A common requirement is to load some module that ships with the current one, | |
// or just a module from somewhere on the machine. | |
// The most efficient (and debuggable) way to do that is by absolute path. | |
// That's easy if it's in a well-known location, | |
// but here's a way to get it relative to your module DLL's location. | |
private static readonly string s_pathToModuleToLoad = Path.GetFullPath( | |
Path.Combine( | |
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), | |
"..", | |
"..", | |
"Modules", | |
"MyCoolModule")); | |
private readonly RunspacePool _runspacePool; | |
protected PowerShellTaskRunner(RunspacePool runspacePool) | |
{ | |
_runspacePool = runspacePool; | |
} | |
// This method runs "Write-Host -Object 'Hello!' synchronously in a runspace in the runspace pool | |
// (This won't actually do anything, since DefaultHost doesn't write anywhere) | |
/// <summary> | |
/// Runs: | |
/// Write-Host -Object 'Hello!' | |
/// | |
/// Synchronous -- so the caller must wait for the command to complete. | |
/// | |
/// (This command doesn't actually do anything other than use CPU, | |
/// since DefaultHost doesn't write anywhere.) | |
/// </summary> | |
public void RunDefaultCommand() | |
{ | |
using (PowerShell pwsh = GetPowerShellInstance()) | |
{ | |
pwsh.AddCommand("Write-Host") | |
.AddParameter("Object", "Hello!") | |
.Invoke(); | |
} | |
} | |
/// <summary> | |
/// Synchronously runs: | |
/// Get-Module -ListAvailable | Out-String | |
/// </summary> | |
/// <returns>Lines of the formatted output of Get-Module -ListAvailable</returns> | |
public Collection<string> RunGetModuleListCommand() | |
{ | |
using (PowerShell pwsh = GetPowerShellInstance()) | |
{ | |
// Note that AddCommand()...AddCommand() works like piping the first command to the second | |
return pwsh | |
.AddCommand("Get-Module") | |
.AddParameter("ListAvailable") | |
.AddCommand("Out-String") | |
.Invoke<string>(); | |
// Invoke<T>() will give you back a strongly typed collection | |
// which is usually preferable in C# | |
} | |
} | |
/// <summary> | |
/// Runs: | |
/// Get-Module | |
/// | |
/// This is run asynchronously from the perspective of the caller, | |
/// so the caller can call this method, and then await the Task later. | |
/// The actual computation will run synchronously on the Task threadpool, | |
/// but it effectively must run synchronously *somewhere*. | |
/// </summary> | |
/// <returns>The modules loaded in the runspace used by the runspace pool.</returns> | |
public Task<Collection<PSModuleInfo>> RunGetModuleAsync() | |
{ | |
return Task.Run(() => | |
{ | |
using (PowerShell pwsh = GetPowerShellInstance()) | |
{ | |
return pwsh.AddCommand("Get-Module") | |
.Invoke<PSModuleInfo>(); | |
} | |
}); | |
} | |
/// <summary> | |
/// Runs: | |
/// Get-Module | |
/// | |
/// This is run asynchronously for the caller, | |
/// but internally also uses PowerShell's own asynchronous API. | |
/// In this example we use .NET's own transformer method, | |
/// which takes care of some things for us, | |
/// but PowerShell's API makes it a bit cumbersome. | |
/// </summary> | |
/// <remarks> | |
/// Also have a read of https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/interop-with-other-asynchronous-patterns-and-types. | |
/// </remarks> | |
/// <returns></returns> | |
public Task<Collection<PSModuleInfo>> RunGetModuleAsync_Alternate1() | |
{ | |
// Create our PowerShell instance, but remember we must dispose of it later | |
PowerShell pwsh = GetPowerShellInstance(); | |
var settings = new PSInvocationSettings | |
{ | |
// Add settings here | |
}; | |
// Use .NET's helpful method for adapting older APM APIs to newer TAP ones | |
return Task.Factory.FromAsync( | |
(callback, state) => pwsh.BeginInvoke(new PSDataCollection<PSObject>(), settings, callback, state), | |
pwsh.EndInvoke, | |
state: null) | |
.ContinueWith(runTask => | |
{ | |
// Dispose of PowerShell now | |
pwsh.Dispose(); | |
// Because of PowerShell's old old BeginInvoke/EndInvoke overloads return unhelpful types, | |
// we're forced to do a conversion here | |
var results = new List<PSModuleInfo>(); | |
foreach (PSObject obj in runTask.Result) | |
{ | |
results.Add((PSModuleInfo)obj.BaseObject); | |
} | |
return new Collection<PSModuleInfo>(results); | |
}); | |
} | |
/// <summary> | |
/// Runs: | |
/// Get-Module | |
/// | |
/// This is run asynchronously for the caller, | |
/// but also uses PowerShell's own async APIs internally, as before. | |
/// This time though, we don't use .NET's transformer method, | |
/// and do things ourselves. | |
/// This means no ContinueWith complexity, | |
/// but instead means we must implement the async callback | |
/// and TaskCompletionSource ourselves. | |
/// </summary> | |
/// <remarks> | |
/// Again look at https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/interop-with-other-asynchronous-patterns-and-types. | |
/// </remarks> | |
/// <returns></returns> | |
public Task<Collection<PSModuleInfo>> RunGetModuleAsync_Alternate2() | |
{ | |
var completionSource = new TaskCompletionSource<Collection<PSModuleInfo>>(); | |
PowerShell pwsh = GetPowerShellInstance(); | |
var settings = new PSInvocationSettings | |
{ | |
// Any settings go here | |
}; | |
// Kick off the async call with a callback | |
pwsh.BeginInvoke(new PSDataCollection<PSObject>(), settings, (IAsyncResult iar) => | |
{ | |
try | |
{ | |
// End the call here. | |
// This synchronously awaits the result on whichever thread is running the callback. | |
// But as above, we have to do this *somewhere*. | |
PSDataCollection<PSObject> results = pwsh.EndInvoke(iar); | |
// Now transform the results | |
var list = new List<PSModuleInfo>(); | |
foreach (PSObject result in results) | |
{ | |
list.Add((PSModuleInfo)result.BaseObject); | |
} | |
var collection = new Collection<PSModuleInfo>(list); | |
// Actually set the task result here | |
completionSource.TrySetResult(collection); | |
} | |
// Handle cancellation and exceptions here | |
catch (OperationCanceledException) | |
{ | |
completionSource.TrySetCanceled(); | |
} | |
catch (Exception e) | |
{ | |
completionSource.TrySetException(e); | |
} | |
// We ensure we always dispose of the PowerShell instance | |
finally | |
{ | |
pwsh.Dispose(); | |
} | |
}, state: null); | |
return completionSource.Task; | |
} | |
/// <summary> | |
/// Runs the given PSCommand synchronously in our runspace pool. | |
/// | |
/// This example separates out the command definition from the invocation | |
/// by taking the command in the form of a PSCommand instead. | |
/// | |
/// Note that there are some issues with PSCommand: | |
/// - There's no CommandInfo overload, so you can't use a cmdlet type directly without reflection | |
/// - There's an issue where the cloning of the PSCommand object isn't done properly: https://github.com/PowerShell/PowerShell/issues/12297 | |
/// </summary> | |
/// <param name="psCommand"></param> | |
/// <typeparam name="T"></typeparam> | |
/// <returns></returns> | |
public Collection<T> RunCommand<T>(PSCommand psCommand) | |
{ | |
using (PowerShell pwsh = GetPowerShellInstance()) | |
{ | |
// Simply assign the command here | |
pwsh.Commands = psCommand; | |
// Then invoke | |
return pwsh.Invoke<T>(); | |
} | |
} | |
/// <summary> | |
/// Runs the given PSCommand asynchronously against our runspace pool. | |
/// | |
/// This example just demonstrates the same PSCommand concept, | |
/// but with an asynchronous call. | |
/// </summary> | |
/// <param name="psCommand"></param> | |
/// <typeparam name="T"></typeparam> | |
/// <returns></returns> | |
public Task<Collection<T>> RunCommandAsync<T>(PSCommand psCommand) | |
{ | |
return Task.Run(() => | |
{ | |
using (PowerShell pwsh = GetPowerShellInstance()) | |
{ | |
pwsh.Commands = psCommand; | |
return pwsh.Invoke<T>(); | |
} | |
}); | |
} | |
/// <summary> | |
/// Runs: | |
/// Get-Module -ListAvailable | |
/// | |
/// This example uses the synchronous RunCommand() method we created, | |
/// just to demonstrate how to use a PSCommand object. | |
/// It works just like the PowerShell object, | |
/// but with no methods/properties to deal with the runtime, | |
/// like Invoke() or Streams. | |
/// </summary> | |
/// <returns></returns> | |
public Collection<PSModuleInfo> RunGetModuleList_Alternate1() | |
{ | |
var command = new PSCommand() | |
.AddCommand("Get-Module") | |
.AddParameter("-ListAvailable"); | |
return RunCommand<PSModuleInfo>(command); | |
} | |
/// <summary> | |
/// Deduplicate our creation of a PowerShell instance that uses our RunspacePool. | |
/// </summary> | |
/// <returns></returns> | |
private PowerShell GetPowerShellInstance() | |
{ | |
// RunspaceMode.NewRunspace is the default but we're being explicit here | |
// | |
// Only use RunspaceMode.CurrentRunspace in single-threaded, synchronous scenarios! | |
// If you use RunspaceMode.CurrentRunspace with a threadpool, | |
// you'll start spinning up runspaces without realising. | |
// And if you create a PowerShell object on one thread with RunspaceMode.CurrentRunspace, | |
// and use it in another thread, you'll hit threading issues. | |
var pwsh = PowerShell.Create(RunspaceMode.NewRunspace); | |
pwsh.RunspacePool = _runspacePool; | |
return pwsh; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment