question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

API for executing a PowerShell script file from C#?

See original GitHub issue

I’m writing a cmdlet that takes in paths of scripts and then executes them from the filesystem.

In PowerShell script this would be trivial — & $path — but from a .NET cmdlet, I can only find hacky ways of doing this rather than a proper API for it.

Current methods

For the sake of documentation here are the methods I’ve got so far:

Escape the path as a PowerShell string and execute it as a command

Perhaps the most straightforward way to do this is to just use PowerShell to invoke it:

string wrapperScript = $"& '{Path.Replace("'", "''")}'";
InvokeCommand.InvokeScript(wrapperScript);

This works, but:

  • We now need to generate PowerShell script on the fly and execute it. This could be especially problematic in scenarios where security and trust are in play
  • If an error occurs, our script stack trace is polluted
  • We’re forced to run a whole parse/execute flow just to get to the file, so it’s not just hacky but also very inefficient

Override PSRemotingCmdlet

Trying to work out the “right” way to do this, I looked at Invoke-Command, since that’s a cmdlet we ship that can invoke script files directly from the filesystem.

This inherits from PSRemotingCmdlet, which contains a lot of code for remote execution, but which also exposes this protected method:

https://github.com/PowerShell/PowerShell/blob/c5955a5c0de50295e509dbc10928147b33d5cbea/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs#L2012-L2046

In Invoke-Command, this is used to execute a script file like this:

https://github.com/PowerShell/PowerShell/blob/c5955a5c0de50295e509dbc10928147b33d5cbea/src/System.Management.Automation/engine/remoting/commands/InvokeCommandCommand.cs#L1178-L1185

As far as I can tell, this is as close as we get to having a public API for executing a PowerShell script file directly. But there are several issues:

  • PSRemotingCmdlet implements things like BeginProcessing(), so to safely reuse it we’re forced to override all its virtual cmdlet methods, and any parameters it defines we’re stuck with – however we could possibly just create a new instance of the cmdlet internally and call this method on it without giving it to the PS runtime…
  • PSRemotingCmdlet is also just a lot of code that we shouldn’t have to touch or think about to execute a script file, and inheriting from it means we can’t inherit from other cmdlet implementations in our code base
  • InvokeUsingCmdlet() is internal and has a number of internally typed parameters, so it’s hard to call. This means we must choose another invocation method for the scriptblock. Executing it is still very possible, but naturally this deviates from how PowerShell does it natively.

Use reflection to create an ExternalScriptInfo object and call that

Looking through things, I think this is basically the “right” way to do things, but a number of steps have no public way to perform and we are required to resort to reflection on APIs that aren’t guaranteed not to break (even though they’re pretty core APIs that probably aren’t going to change any time soon — but never say never).

The steps are essentially:

  • Create an ExternalScriptInfo object. This is a public type, but has no public constructors and I wasn’t able to find another way to construct one
  • Pass that to PowerShell.Create().AddCommand(), which accepts a CommandInfo object
  • Execute that PowerShell object

The issues here are that:

  • ExternalScriptInfo has no public constructor, and the correct constructor requires the internal type ExecutionContext https://github.com/PowerShell/PowerShell/blob/c5955a5c0de50295e509dbc10928147b33d5cbea/src/System.Management.Automation/engine/ExternalScriptInfo.cs#L24-L55
  • So we also need to use reflection to (1) get the ExecutionContext instance from our cmdlet and (2) efficiently pass it into the invocation of the constructor (by which I mean that compiling a Func<ExternalScriptInfo> around this is made harder because one of the inputs must be cast to a statically-unknown type)
  • Again, the way PowerShell itself uses this is to run InvokeUsingCmdlet(), which is also still internal. The silliest part here is that we just used an ExecutionContext to construct the ExternalScriptInfo and now invoking it from a cmdlet will just replace that ExecutionContext. Instead the simplest way to go is going to be ScriptBlock.Invoke() or the PowerShell.Create(RunspaceMode.CurrentRunspace).AddCommand(externalFileInfo).Invoke() method, both of which use more implicit magic around runspace and context.
  • While we can use a CommandInfo object for PowerShell.AddCommand(), we can’t use it for PSCommand.AddCommand() safely with older PowerShell versions because of https://github.com/PowerShell/PowerShell/issues/12297.

A better way?

So first off, others might have a better way to do this today, and if so we should definitely discuss it here. Ideally we can get scenarios like this documented nicely in some developer-oriented documentation.

But also, my ideal for this scenario would be new methods in particular places that allow a filepath to be passed in:

  • CommandInvocationIntrinsics.InvokeScriptFile(string filePath, ...), which would allow anything with a CommandInvocationIntrinsics object to just run a scriptfile
  • PowerShell.AddFile(string filePath, ...)/PSCommand.AddFile(string filePath, ...), to allow the same using the usual PowerShell API
  • A public ExternalScriptInfo constructor or Create() method that allows for the creation of a CommandInfo object that directly describes a scriptfile, for parity with CmdletInfo. FunctionInfo might also be worth doing this for, although ScriptBlock.Create() essentially fills this role today.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:8 (5 by maintainers)

github_iconTop GitHub Comments

3reactions
mklement0commented, Mar 3, 2021

Note that the SDK’s .AddCommand() method already accepts a file path string directly:

'"hi from test.ps1: $foo"' > test.ps1

$foo = 'value from this runspace'

$ps = [powershell]::Create('CurrentRunSpace')

$ps.AddCommand((Convert-Path test.ps1)).Invoke()

The name .AddCommand() doesn’t make that capability obvious, however, and the .AddScript() method, which accepts a script block’s text, i.e. a source-code string rather than a file path, adds to the confusion.

2reactions
SeeminglySciencecommented, Mar 3, 2021

The name .AddCommand() doesn’t make that capability obvious, however, and the .AddScript() method, which accepts a script block’s text, i.e. a source-code string rather than a file path, adds to the confusion.

Yeah… the name makes sense in the context of implementation detail, but boy is it hard to explain outside of it.

The tl;dr of it is: if you can run Get-Command X and get a result, X probably works in AddCommand. If you can’t, then it probably needs to be parsed and compiled/interpreted.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Simple HTTP api for Executing PowerShell Scripts
I wrote a simple HTTP Listener in PowerShell script that uses the .Net HttpListener class. You simply start an instance of the listener...
Read more >
Execute a PowerShell Script in C#
In this article, we'll learn how to execute a PowerShell script in C# using the ProcessStartInfo class from System.
Read more >
How to execute a PowerShell script using C# - ...
Arguments = @"powershell -File ""C:\Users\user1\Desktop\power.ps1"""; startInfo.Verb = "runas"; startInfo.RedirectStandardOutput = true; ...
Read more >
REST API to Run PowerShell from a ASP.Net C# Webapp
Developing a web application with a REST API to run PowerShell scripts is a straightforward process. First, the necessary components of the web ......
Read more >
Fix for PowerShell Script cannot be loaded because ...
On trying to run a PowerShell script from the PowerShell console, I received this error message: “File C:\temp\GenerateRpt.ps1 cannot be loaded because ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found