How to start a new PowerShell session from another PS session, and run a CmdLet with splatted Parameters

三世轮回 提交于 2019-12-10 10:02:37

问题


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.


回答1:


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.




回答2:


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.




回答3:


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.



来源:https://stackoverflow.com/questions/48369027/how-to-start-a-new-powershell-session-from-another-ps-session-and-run-a-cmdlet

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!