PowerShell Runspaces, outputs and variables - powershell

I'm trying to understand Runspaces in PowerShell. I know about the PoshRSJob-Module, but I'd like to create my Runspace Jobs by myself.
This is my code, mostly taken out from this blog:
$Computer = "somename"
[runspacefactory]::CreateRunspacePool() > $null
$SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$RunspacePool = [runspacefactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
1..2 | % {
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$PowerShell.AddScript({
param(
$Computer
)
$Computer
}) > $null
$PowerShell.AddParameter($Computer)
$Invoke = $PowerShell.BeginInvoke()
while (!($Invoke.IsCompleted)) {sleep -Milliseconds 2}
$Data = $PowerShell.EndInvoke($Invoke)
Write-Host $Data -f Red
}
Three question are in my mind:
Will I be able to return a Value in a Variable and use it for further use inside the script after finishing the Job?
Why is my $Data Variable empty?
In the Script I have so far, what is creating the following output? If I $null the invocation like this $Invoke = $PowerShell.BeginInvoke() > $null
, the script doesn't work properly anymore and still creates this output
Commands : System.Management.Automation.PSCommand
Streams : System.Management.Automation.PSDataStreams
InstanceId : 3b91cfda-028e-4cec-9b6d-55bded5d9d3c
InvocationStateInfo : System.Management.Automation.PSInvocationStateInfo
IsNested : False
HadErrors : False
Runspace :
RunspacePool : System.Management.Automation.Runspaces.RunspacePool
IsRunspaceOwner : False
HistoryString :

I don't understand your first question.
For the second question, I think it's because you're using $PowerShell.AddParameter($Computer).
Try $PowerShell.AddArgument($Computer) instead. AddArgument is for adding a value that gets implicitly (positionally) bound to a parameter. AddParameter is for adding a named parameter. The overload of AddParameter that takes just a string is for [Switch] parameters.
For your third question, I think it's $RunspacePool.Open() that's giving you that output.
When trying to determine these things, look for lines, especially with method calls, that have no left-hand assignment; so things you aren't assigning to a variable, as that's generally how these values get put into the output stream.

Related

Problems with interconnected parameters in PowerShell Param block

I'm having troubles wrapping my head around the code logic needed to get my Param block functioning as I want in PowerShell 5.1. A very simplified (but usable for my request) script with the Param block is here and this works exactly as I want:
[CmdletBinding(DefaultParameterSetName = 'All')]
Param (
[Parameter(Position=0,Mandatory=$True)]
[String]$Name,
[Parameter(ParameterSetName = 'All')]
[Switch]$All,
[Parameter(ParameterSetName = 'Individual')]
[Switch]$P1,
[Parameter(ParameterSetName = 'Individual')]
[Switch]$P2
)
If ($All -Or ($P1.ToBool() + $P2.ToBool() -eq 0)) {
$All = $True
$P1 = $True
$P2 = $True
}
"`$Name is $Name"
"`$All is $All"
"`$P1 is $P1"
"`$P2 is $P2"
The auto-completion of parameters when running this script above works as intended. If I use the "-All" switch, then -P# are not available. If I use -P# switche, -All is not available. If I omit the -Name, then it prompts me to put in a name. If I use the -All switch, I want all of the individual -P# options to later be set to $True. If I use no switches at all, it prompts me for a Name then sets all options to $True.
The first problem is that when using DefaultParameterSetName = 'All' (which I had to do in order to make the script work without any switches on the command line), then the $All variable is NOT actually being set to $True when it is not present on the command line. I had to make the "If" block in order to overcome that behavior. This makes the next problem come up because the actual script I'm trying to use this in will have fifteen or more -P# switches. That will make the "If" test more complex and ugly.
Is there a better way I can do this? Maybe something in the layout of my Parameter Sets? I could even eliminate the "-All" switch entirely if there's an easier way to evaluate that none of the -P# switches are used. Is there an easier way to add up the boolean value of all Parameters named P#? I've stumbled across the $MyInvocation variable and $MyInvocation.MyCommand.Parameters seems promising but I'm not sure exactly how to process that either.
Update after answer found:
Here is my new, simplified working code sample which I arrived at thanks to all the suggestions here. The "-All" Switch was unnecessary. I decided to go with the Get-Variable method here for now due to its simplicity and scalability, but it does require a common prefix on the Switch variables. The If() block will remain the same no matter how many variables are used.
Param (
[Parameter(Position=0,Mandatory=$True)]
[String]$Name,
[Alias('Blue')]
[Switch]$OptionBlue,
[Alias('Red')]
[Switch]$OptionRed,
[Alias('Yellow')]
[Switch]$OptionYellow
)
$AllOptions = Get-Variable -Name 'Option*'
If (-Not $AllOptions.Value.Contains($True)) {
"None of the Option Switches were used, setting all Option Variables to $True"
ForEach ($Option In $AllOptions) {$Option.Value = $True}
}
"`$Name: $Name"
"`$OptionBlue: $OptionBlue"
"`$OptionRed: $OptionRed"
"`$OptionYellow: $OptionYellow"
and here's how it works:
PS C:\Scripts\PowerShell> .\Test-Params.ps1
cmdlet Test-Params.ps1 at command pipeline position 1
Supply values for the following parameters:
Name: Testing
None of the Option Switches were used, setting all Option Variables to True
$Name: Testing
$OptionBlue: True
$OptionRed: True
$OptionYellow: True
PS C:\Scripts\PowerShell> .\Test-Params.ps1 -Name Testing -OptionRed
$Name: Testing
$OptionBlue: False
$OptionRed: True
$OptionYellow: False
PS C:\Scripts\PowerShell> .\Test-Params.ps1 -Name Testing -Blue -Yellow
$Name: Testing
$OptionBlue: True
$OptionRed: False
$OptionYellow: True
PS C:\Scripts\PowerShell> .\Test-Params.ps1 -Yellow -OptionRed
cmdlet Test-Params.ps1 at command pipeline position 1
Supply values for the following parameters:
Name: Testing
$Name: Testing
$OptionBlue: False
$OptionRed: True
$OptionYellow: True
Final Update
I figured out the way to avoid any issue where the script's switch parameters might match an already existing variable in the scope. Pulling the script's parameters that match the proper prefix/suffix used in the script's parameter names using $MyInvocation and then passing those specific names to Get-Variable avoids the issue. The switches also work correctly both when they are not present, or if they are explicitly set to $False. If more switches are needed, simply add them in the Param block with the proper prefix/suffix in the name and an alias for the simpler version. I think this bit of the code is bulletproof now...
Param (
[Parameter(Position=0,Mandatory=$True)]
[String]$Name,
[Alias('Blue')]
[Switch]$OptionBlue,
[Alias('Red')]
[Switch]$OptionRed,
[Alias('Yellow')]
[Switch]$OptionYellow
)
$OptionNotAnOption = $True
$OptionSwitches = ForEach ($Option In ($MyInvocation.MyCommand.Parameters.Keys | Where {$_ -Like "Option*"})) {Get-Variable $Option}
If ($OptionSwitches.Value.IsPresent -NotContains $True) {
"All options are `$False or not present: Enabling all options"
ForEach ($Switch In $OptionSwitches) {
Get-Variable -Name ($Switch.Name) | Set-Variable -Value $True
}
}
"`$Name: $Name"
"`$OptionBlue: $OptionBlue"
"`$OptionRed: $OptionRed"
"`$OptionYellow: $OptionYellow"
"`$OptionNotAnOption: $OptionNotAnOption"
You can use the $PSBoundParameters "automatic variable" to access the parameters specified in the call to the script / function and alter the function's behaviour accordingly.
For example:
function Invoke-MyFunction
{
param
(
[string] $Name,
[switch] $P1,
[switch] $P2
)
# how many $Pn parameters were specified in the call to the function?
$count = #( $PSBoundParameters.GetEnumerator()
| where-object { $_.Key.StartsWith("P") }
).Length;
# if *none* specified then enable *all*
$all = $count -eq 0;
if( $all )
{
$P1 = $true;
$P2 = $true;
}
"`$Name is $Name"
"`$all is $all"
"`$P1 is $P1"
"`$P2 is $P2"
}
And some tests:
PS> Invoke-MyFunction
$Name is
$all is True
$P1 is True
$P2 is True
PS> Invoke-MyFunction -Name "aaa"
$Name is aaa
$all is True
$P1 is True
$P2 is True
PS> Invoke-MyFunction -Name "aaa" -P1
$Name is aaa
$all is False
$P1 is True
$P2 is False
PS> Invoke-MyFunction -Name "aaa" -P2
$Name is aaa
$all is False
$P1 is False
$P2 is True
PS> Invoke-MyFunction -Name "aaa" -P1 -P2
$Name is aaa
$all is False
$P1 is True
$P2 is True
but watch out because the way I've evaluated $count means that specifying -P1:$false or -P2:$false makes $all = $false:
PS> Invoke-MyFunction -Name "aaa" -P1:$false
$Name is aaa
$all is False
$P1 is False
$P2 is False
so you might need to refine the expression to suit whatever you want to happen in this edge case...
Update
If your parameter names don't follow a simple pattern you can do something like this instead:
$names = #( "SomeParam", "AnotherParam", "Param3" );
$count = #( $PSBoundParameters.GetEnumerator()
| where-object { $_.Key -in $names }
).Length;
Assuming those are all switches and they belong to the parameter set of 'Individual', you can set the unset parameters to $true if $All is specified like so:
if ($All.IsPresent)
{
(Get-Command -Name $MyInvocation.MyCommand.Name).Parameters.Values |
Where-Object -FilterScript { $_.ParameterSets.Keys -eq 'Individual' } |
Foreach-Object -Process {
Set-Variable -Name $_.Name -Value $true -PassThru # remove -PassThru to silence the output
}
}
Referencing the current executing command with $MyInvocation.MyCommand.Name, you can pass it to Get-Command for more detailed info. on its parameters. This will allow you to filter by ParameterSets grabbing just the ones falling under Individual. Finally, it will pass it to Set-Variable setting all the switches to $true in a dynamic sense.
Note: I typed this up on my phone so there may be some typos that should be easy to correct.

How to call a script with unknown parameters

I have a script that calls other scripts that other people manage. It's essentially a CI/CD script that gives users the ability to tap into the pipeline.
The issue I'm running into now is that I would like this calling script to implement a couple new parameters. However, the old scripts don't always implement those parameters.
If I call their script that doesn't implement the parameters, I get an error "A parameter cannot be found that matches parameter name 'newparameter'".
Is there a way to dynamically pass in a parameter so that it doesn't fail if the parameter doesn't exist? I don't mind if they don't implement it. It's a bonus parameter that they don't need to use.
Alternately, can I do something like a Get-Command for a custom .ps1 script, to get a list of accepted parameters? With that, I could confirm that a parameter is implemented before I pass it.
This might help you get started, you could use the Parser Class
to get all functions and it's parameters from a script, this answer shows a minimal reproduction. I'll leave it to you to investigate further.
Given myScript.ps1 that has these 3 functions:
function ExampleFunc {
param([int] $param1 = 123, [string] $param2)
}
function ExampleFunc2 {
param([object] $param3, [switch] $param4)
}
function ExampleFunc3 ($param5, [hashtable] $param6 = #{foo = 'var'}) {
}
You can use the ParseFile Method to get the AST, then you can use the .FindAll method to filter for all FunctionDefinitionAst and subsequently find all parameters filtering for all ParameterAst.
using namespace System.Management.Automation.Language
$ast = [Parser]::ParseFile('path\to\myScript.ps1', [ref] $null, [ref] $null)
$ast.FindAll({ $args[0] -is [FunctionDefinitionAst] }, $true) | ForEach-Object {
$out = [ordered]#{ Function = $_.Name }
$_.FindAll({ $args[0] -is [ParameterAst] }, $true) | ForEach-Object {
$out['ParameterName'] = $_.Name.VariablePath
$out['Type'] = $_.StaticType
$out['DefaultValue'] = $_.DefaultValue
[pscustomobject] $out
}
} | Format-Table
Above code would result in the following for myScript.ps1:
Function ParameterName Type DefaultValue
-------- ------------- ---- ------------
ExampleFunc param1 System.Int32 123
ExampleFunc param2 System.String
ExampleFunc2 param3 System.Object
ExampleFunc2 param4 System.Management.Automation.SwitchParameter
ExampleFunc3 param5 System.Object
ExampleFunc3 param6 System.Collections.Hashtable #{foo = 'var'}
The same could be accomplished using Get-Command:
(Get-Command 'fullpath\to\myScript.ps1').ScriptBlock.Ast.FindAll({
... same syntax as before ... }, $true # or $false for non-recursive search
)

Powershell - pass a value to parameter

How to pass value along with parameter? Something like ./test.ps1 -controllers 01. I want the script to use hyphen and also a value is passed along for the parameter.
Here is the part of the script I wrote. But if I call the script with hyphen (.\test.ps1 -Controllers) it says A parameter cannot be found that matches parameter name 'Controllers'.
param(
# [Parameter(Mandatory=$false, Position=0)]
[ValidateSet('Controllers','test2','test3')]
[String]$options
)
Also I need to pass a value to it which is then used for a property.
if ($options -eq "controllers")
{
$callsomething.$arg1 | where {$_ -eq "$arg2" }
}
Lets talk about why it does not work
function Test()
param(
[Parameter(Mandatory=$false, Position=0)]
[ValidateSet('Controllers','test2','test3')]
[String]$options
)
}
Parameters are Variables that are created and filled out at the start of the script
ValidateSet will only allow the script to run if $Options equals one of the three choices 'Controllers','test2','test3'
Lets talk about what exactly all the [] are doing
Mandatory=$false means that $options doesnt have to be anything in order for the script to run.
Position=0 means that if you entered the script without using the -options then the very first thing you put would still be options
Example
#If Position=0 then this would work
Test "Controllers"
#Also this would work
Test -options Controllers
[ValidateSet('Controllers','test2','test3')] means that if Option is used or is Mandatory then it has to equal 'Controllers','test2','test3'
It sounds like you are trying to create parameters at runtime. Well that is possible using DynamicParam.
function Test{
[CmdletBinding()]
param()
DynamicParam {
$Parameters = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
'Controllers','test2','test3' | Foreach-object{
$Param = New-Object System.Management.Automation.ParameterAttribute
$Param.Mandatory = $false
$AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$AttribColl.Add($Param)
$RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter("$_", [string], $AttribColl)
$Parameters.Add("$_", $RuntimeParam)
}
return $Parameters
}
begin{
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value $_.Value
}
}
process {
"$Controllers $Test2 $Test3"
}
}
DynamicParam allows you to create parameters in code.
The example above turns the array 'Controllers','test2','test3' into 3 separate parameters.
Test -Controllers "Hello" -test2 "Hey" -test3 "Awesome"
returns
Hello Hey Awesome
But you said you wanted to keep the hypen and the parameter
So the line
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value $_.Value
}
allows you to define each parameter value. a slight change like :
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value "-$($_.Key) $($_.Value)"
}
Would return
-Controllers Hello -test2 Hey -test3 Awesome

How can I get the Log directory?

I have recently installed SSAS on my servers and instead of going through 24 of them, I am writing a script to get back the logDir of the servers.
I am trying to do something like this:
Import-Module SqlServer
$Analysis_Server = New-Object Microsoft.AnalysisServices.Server
$Analysis_Server.connect("$server")
$Analysis_Server.ServerProperties.LogDir
I am trying to find the logDir property from here to no avail.
If I use ServerProperties, I get a list of all properties available like this:
Name : LogDir
Type :
Value : S:....
DefaultValue : \data
PendingValue : S:...
RequiresRestart : True
IsReadOnly : False
DisplayFlag : True
Category : Basic
Units : Folder
PropertyName : LogDir
FolderName
But if I do: $Analysis_Server.ServerProperties.LogDir.value or $Analysis_Server.ServerProperties.LogDir, it returns nothing.
Update
This is how I plan to run through multiple servers:
$h = #{}
Import-Csv '$csvFile' | ForEach-Object {
$h += #{$($_.Server -split '\s*,\s*') }
}
Import-Module SqlServer
foreach($server in $h.Keys){
$result = "$server"
Write-Host $result
$Analysis_Server = New-Object Microsoft.AnalysisServices.Server
$Analysis_Server.connect("$server")
$Analysis_Server.ServerProperties['LogDir'].value
}
This is my CSV file (I plan to use this for multiple purposes, so I only want to get the servers not databases for this case):
I got back this error:
Missing '=' operator after key in hash literal.
This might get you part of they way there. I don't have an analysis server at my disposal you may have to change this up a bit till you get it right.
$servers=get-content c:\temp\servers.txt
$hash=new-object hashtable
$Analysis_Server = New-Object Microsoft.AnalysisServices.Server
foreach($s in $servers)
{
$Analysis_Server.connect("$s")
$hash.add($s, $"$($Analysis_Server.ServerProperties['LogDir'])")
}
To see more great content on hashtables checkout this link: https://kevinmarquette.github.io/2016-11-06-powershell-hashtable-everything-you-wanted-to-know-about/

Passing "native" object to background jobs

Here is what I'd like to achieve in one way or another.
I have a custom assembly defining some objects. In my script, I create a custom object that I'd like to pass to a script block, keeping that object behavior.
Add-Type -AssemblyName MyCustomDLL
$global:object = new-object MyCustomDLL.MyCustomObject()
$object | gm
$jobWork = { param ($object) $object | gm } # I'd like to keep my object behavior in that block
$job = Start-Job -ScriptBlock $jobWork -ArgumentList $object
Wait-Job $job
Receive-Job $job
How can I do that or achieve the same effect? Thanks for your help
Instead of background jobs you may use PowerShell with BeginInvoke, EndInvoke. Here is the simple but working example of passing a live object in a "job", changing it there, getting the results:
# live object to be passed in a job and changed there
$liveObject = #{ data = 42}
# job script
$script = {
param($p1)
$p1.data # some output (42)
$p1.data = 3.14 # change the live object data
}
# create and start the job
$p = [PowerShell]::Create()
$null = $p.AddScript($script).AddArgument($liveObject)
$job = $p.BeginInvoke()
# wait for it to complete
$done = $job.AsyncWaitHandle.WaitOne()
# get the output, this line prints 42
$p.EndInvoke($job)
# show the changed live object (data = 3.14)
$liveObject
Background jobs are built on top of PowerShell remoting and as such, perform similar actions when passing objects around. They would serialize/ deserialize them rather than pass them with all their complexity.
My guess is that the only way to get complex object is just to pass constructor arguments and/ or operations as -ArgumentList and create object inside job.
In such a case also adding assembly would have to be part of the job:
Start-Job {
param ($ConstructorArguments)
Add-Type -AssemblyName MyCustomDll
$object = New-Object MyCustomDll.MyCustomObject $ConstructorArguments
$object | Get-Member
} -ArgumentList Foo, Bar | Wait-Job | Receive-Job