3

The Goal:

Is to be able to test to see if PowerShell v6 is installed (which is OK), and if it is, then to invoke that shell for certain CmdLets. This will be invoked within a script running in PowerShell v5.1. I cannot shift fully to v6 as there are other dependencies that do not yet work in this environment, however, v6 offers significant optimisations on certain CmdLets that lead to an improvement in operation of over 200 times (specifically, an Invoke-WebRequest where the call will lead to a download of a large file - in v5.1 a 4GB file will take over 1 hour to download, in v6 this will take approximately 30 seconds using the same machines on the same subnet.

Additional Points:

However, I also build up a set of dynamic parameters that are used to splat into the CmdLets parameter list. For example, a built parameter list would look something like:

$SplatParms = @{
    Method = "Get"
    Uri = $resource
    Credential = $Creds
    Body = (ConvertTo-Json $data)
    ContentType = "application/json"
}

And running the CmdLet normally would work as expected:

Invoke-RestMethod @SplatParms

What has been tried:

Over the past few days I have looked over various posts on this forum and elsewhere. We can create a simple script block the can be call and also works as expected:

$ConsoleCommand = { Invoke-RestMethod @SplatParms }

& $ConsoleCommand

However, attempting to pass the same thing in the Start-Process CmdLet fails, as I guess the parameter hash table is not being evaluated:

Start-Process pwsh -ArgumentList "-NoExit","-Command  &{$ConsoleCommand}" -wait

Results in:

Invoke-RestMethod : Cannot validate argument on parameter 'Uri'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.
At line:1 char:22
+ &{ Invoke-RestMethod @SplatParms }

Where next?

I guess I somehow have to pass in parameters as arguments so they can then be evaluated and splatted, however, the syntax eludes me. I'm not even sure if Start-Process is the best CmdLet to use, but rather should I look to something else, like Invoke-Command, or something completely different?

It would be awesome to get the result of this CmdLet back into the originating shell, but at the moment, it will simply take something that functions.

Swinster
  • 99
  • 3
  • 9

3 Answers3

5

Note: In principle, the techniques in this answer can be applied not only to calling from Windows PowerShell to PowerShell Core, but also in the opposite direction, as well as to between instances of the same PowerShell edition, both on Windows and Unix.

You don't need Start-Process; you can invoke pwsh directly, with a script block:

pwsh -c { $SplatParms = $Args[0]; Invoke-RestMethod @SplatParms } -args $SplatParms

Note the need to pass the hashtable as an argument rather than as part of the script block.

  • Unfortunately, as of Windows PowerShell 5.1 / PowerShell Core 6.0.0, there is a problem with passing [PSCredential] instances this way - see bottom for a workaround.

This will execute synchronously and even print the output from the script block in the console.

The caveat is that capturing such output - either by assigning to a variable or redirecting to a file - can fail if instances of types that aren't available in the calling session are returned.

As a suboptimal workaround, you can use -o Text (-OutputFormat Text) thanks, PetSerAl to capture the output as text, exactly as it would print to the console (run pwsh -h to see all options).

Output is by default returned in serialized CLIXML format, and the calling PowerShell sessions deserializes that back into objects. If the type of a serialized object is not recognized, an error occurs.

A simple example (execute from Windows PowerShell):

# This FAILS, but you can add `-o text` to capture the output as text.
WinPS> $output = pwsh -c { $PSVersionTable.PSVersion } # !! FAILS
pwsh : Cannot process the XML from the 'Output' stream of 'C:\Program Files\PowerShell\6.0.0\pwsh.exe': 
SemanticVersion XML tag is not recognized. Line 1, position 82.
...

This fails, because $PSVersionTable.PSVersion is of type [System.Management.Automation.SemanticVersion] in PowerShell Core, which is a type not available in Windows PowerShell as of v5.1 (in Windows PowerShell, the same property's type is [System.Version]).


Workaround for the inability to pass a [PSCredential] instance:

pwsh -c { 
          $SplatParms = $Args[0];
          $SplatParams.Credential = [pscredential] $SplatParams.Credential;
          Invoke-RestMethod @SplatParms
        } -args $SplatParms

Calling another PowerShell instance from within PowerShell using a script block involves serialization and deserialization of objects in CLIXML format, as also used in PowerShell remoting.

Generally, there are many .NET types that deserialization cannot faithfully recreate and in such cases creates [PSCustomObject] instances that emulate instances of the original type, with the (normally hidden) .pstypenames property reflecting the original type name prefixed with Deserialized.

As of Windows PowerShell 5.1 / PowerShell Core 6.0.0, this also happens with instances of [pscredential] ([System.Management.Automation.PSCredential]), which prevents their direct use in the target session - see this GitHub issue.

Fortunately, however, simply casting the deserialized object back to [pscredential] seems to work.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    You can use `-OutputFormat Text` to return output as text instead of CLIXML. – user4003407 Jan 21 '18 at 19:03
  • It maybe that there is some oddity in passing a `System.Management.Automation.PSCredential` as part of the parameter hash table as I get an error on argument transformation on the Credential parameter. ```PS C:\Users\user> pwsh -command { $SplatParms = $Args[0]; Invoke-RestMethod @SplatParms } -args $SplatParms ``` results in : ```Invoke-RestMethod : Cannot process argument transformation on parameter 'Credential'. userName At line:1 char:44 + $SplatParms = $Args[0]; Invoke-RestMethod @SplatParms``` (sorry for the poor formatting) – Swinster Jan 22 '18 at 00:46
  • I should say, the above occurs when you attempt to invoke `powershell` (v5.1.) from a PowerShell v5.1 prompt, as well as `pwsh`. – Swinster Jan 22 '18 at 09:04
  • @Swinster: Thanks for investigating that; please see my update. – mklement0 Jan 22 '18 at 20:12
0

Try creating a New-PSSession against 6.0 within your 5.1 session.

After installing powershell core 6.0 and running Enable-PSRemoting, a new PSSessionConfiguration was created for 6.0:

PS > Get-PSSessionConfiguration


Name          : microsoft.powershell
PSVersion     : 5.1
StartupScript : 
RunAsUser     : 
Permission    : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed

Name          : microsoft.powershell.workflow
PSVersion     : 5.1
StartupScript : 
RunAsUser     : 
Permission    : BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed

Name          : microsoft.powershell32
PSVersion     : 5.1
StartupScript : 
RunAsUser     : 
Permission    : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed

Name          : PowerShell.v6.0.0
PSVersion     : 6.0
StartupScript : 
RunAsUser     : 
Permission    : NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed

In your parent script, create new session using the 6.0 configuration name PowerShell.v6.0.0 and pass it to any subsequent Invoke-Command you require. Results are returned as objects. Scriptblocks may require local variables passed through -ArgumentList, per mklement0's answer.

$ps6sess = New-PSSession -ComputerName localhost -ConfigurationName 'PowerShell.v6.0.0'
$results = Invoke-Command -Session $ps60sess -ScriptBlock {Param($splatthis) Invoke-WebRequest @splatthis} -ArgumentList $SplatParms

It may also be useful to know that the session persists between Invoke-Command calls. For example, any new variables you create will be accessible in subsequent calls within that session:

PS > Invoke-Command -Session $ps60sess -ScriptBlock {$something = 'zers'}

PS > Invoke-Command -Session $ps60sess -ScriptBlock {write-host $something }
zers

Trouble with PSCredential passing doesn't seem to be a problem with this approach:

$ps6sess = New-PSSession -ComputerName localhost -ConfigurationName 'PowerShell.v6.0.0'
$credential = Get-Credential -UserName 'TestUser'

$IRestArgs = @{
    Method='GET'
    URI = 'https://httpbin.org'
    Credential = $credential
}
$IRestBlock = {Param($splatval) Invoke-RestMethod @splatval}
Invoke-Command -Session $ps6sess -ScriptBlock $IRestBlock -ArgumentList $IRestArgs
# no error

pwsh -c { 
      Param ($SplatParms)
      #$SplatParams.Credential = [pscredential] $SplatParams.Credential;
      Invoke-RestMethod @SplatParms
} -args $IRestArgs
# error - pwsh : cannot process argument transformation on 
#   parameter 'Credential. username

Perhaps at the ps6 session knows it is receiving a block from ps5.1 and knows how to accommodate.

veefu
  • 2,820
  • 1
  • 19
  • 29
  • While the information about remoting is interesting, I don't see any advantage to using it in this scenario; on the contrary: setup work is needed, and the command must be run with elevated privileges. Ultimately, AFAIK, the `pwsh -c { ... }` approach uses the same serialization / deserialization techniques under the hood as remoting does. – mklement0 Jan 22 '18 at 20:17
  • I appreciate the feedback, but I think ignoring sessions does yourself a disservice. Many powerful cmdlets already have session support built-in. Managing an ad-hoc pwsh is cumbersome and has limitations; The 'pwsh -c' solution will need to be rebuilt if you want to run your the script remotely or asynchronously. The command need not be elevated: normal users may be granted session use. It's not just interesting, it's fundamental. – veefu Jan 23 '18 at 00:24
  • Also, the problem you're having passing pscredentials to the 'pwsh' is not an issue when using pssession, at least not in my tests. I'll edit my answer to add some test code. – veefu Jan 23 '18 at 00:50
  • 1
    Thanks for investigating the discrepancy in `[pscredential]` deserialization - given that it works with remoting and that a simple cast fixes the issue for direct invocation, I suspect this is not a by-design security feature, but a _bug_, which I've reported [on GitHub](https://github.com/PowerShell/PowerShell/issues/5988); please feel free to provide additional insight there. – mklement0 Jan 23 '18 at 14:06
  • 1
    Thanks, this has been very enlightening. – veefu Jan 23 '18 at 20:24
-1

An immediate flash using start-process isn't proper, would have to research for that, regardless, my reaction is to construct an array of params to use not splat them for a try.

  • Please don't post incomplete answers with solutions that you haven't at least tested yourself; for quick hints, please use _comments_. – mklement0 Jan 23 '18 at 14:20
  • Apology, saw missing concerns, an architect of apps and experienced PS automation focus, will defer insights. – Tom Mallard Jan 24 '18 at 17:46