Powershell: Passing parameters to function - powershell

So I'm doing a bit of 'House Keepying' on my script and I've found one area than can be reduced/tidied.
Take this GUI I've created:
Both menu bar add_click events to Restart HostnameA and HostnameB call seperate functions even though the code in both of the functions is pratically the same, the only difference is this variable for the hostname (see below).
Code for button events.
$Restart_current_machine.Add_Click(
{
restart_current_machines
})
$Restart_target_machine.Add_Click(
{
restart_target_machines
})
# Function Blocks
function restart_target_machines
{
restart-computer -computer $combobox1.text -force
}
function restart_current_machines
{
restart-computer -computer $combobox2.text -force
}
My question is this:
is there a way I can use Param() (or something like that) to get rid of function restart_current_machinesthereby only having one function to restart either of the machines?
Something like?
$Restart_current_machine.Add_Click(
{
param($input = combobox1.text)
$input | restart_current_machines
})
$Restart_target_machine.Add_Click(
{
param($input = combobox2.text)
$input | restart_current_machines
})
# Only needing one function
function restart_target_machines
{
restart-computer -computer $input -force
}
I know that is in all probability wrong, but just to give you a better idea of what I'm trying to do.

Create a generic function that defines a ComputerName parameter and pass that parameter to the underlying cmdlet:
function restart-machine ([string[]]$ComputerName)
{
Restart-Computer -ComputerName $ComputerName -Force
}
The Rastart-Compter cmdlet ComputerName parameter accepts a collection of names so the parameter is defained as a string array.
Now, from anywhere in your code just call restart-machine and pass the computer names to restart to the ComputerName parameter. To restart multiple machines, delimit each name with a comma (i.e restart-machine -computerName $combobox1.text,$combobox2.text)
$Restart_target_machine.Add_Click(
{
restart-machine -computerName $combobox1.text
})

Related

Invoke-Command Using Local Function With Array As Single Parameter

I have a function that takes a single string array as a parameter in my PowerShell .pm1 that I want to be able to call on a remote server using a second function in my .pm1 (I do not want to rely on the server having a copy of the function). I found this Using Invoke-Command -ScriptBlock on a function with arguments but it only seems to work for 'non-arrays' or for multiple parameters (where array variable is not last)
function Hello_Worlds { param([string[]]$persons)
foreach($person in $persons){
write-host ("hello "+$person)
}
}
$people = "bob","joe"
Invoke-Command -ComputerName "s1" -ScriptBlock ${function:Hello_Worlds} -ArgumentList $people
#output => "hello bob" only
Invoke-Command -ComputerName "s1" -ScriptBlock ${function:Hello_Worlds} -ArgumentList $people, ""
#output => "hello bob hello joe"
I can modify my argument list like -ArgumentList $people, "" (above) to make it work by forcing the function to see the $persons variable as a single parameter and not an array of parameters, but that seems like bad practice and I sure that I am just missing something simple.
EDIT:
I was directed here ArgumentList parameter in Invoke-Command don't send all array and while it works for this exact example, it requires that I KNOW which parameters require an array. Is there a generic way to pass an any arguments that would prevent this issue? I.E. I build my argument list as an array of parameters and there could be 0 or more of them and any number of them could be arrays - or am I stuck with putting this in front of calls?
foreach($parg in $myCustomGeneratedArguments) {
if($parg -is [array]) {$paramArgs += ,$parg}
else {$paramArgs += $parg}
}
Looking at your edit I'm afraid the linked answer doesn't lead you to the easier path, which is to not use -ArgumentList at all, instead, refer to your Hello_Worlds function and to your $people array with the $using: scope modifier:
function Hello_Worlds { param([string[]]$persons)
foreach($person in $persons){
write-host ("hello "+$person)
}
}
# store the function definition locally
$func = ${function:Hello_Worlds}.ToString()
$people = "bob","joe"
Invoke-Command -ComputerName "s1" -ScriptBlock {
# define the function in the remote scope
${function:Hello_Worlds} = $using:func
# now you can use it normally
Hello_Worlds -persons $using:people
}

How can I use async or parallelism in Powershell class constructors?

I have a Powershell class (a couple, actually; it's nested) that I'm creating instances of in a loop. The constructor has a bunch of tasks that populate the object (including those nested classes, which also populate themselves). However, those tasks are sometimes a bit slow, and I'd like to be able to execute multiple tasks and instantiate multiple objects concurrently. How can I do that within Powershell?
Example class:
Class Server {
Server([string] $ServerName) {
$this.ServerName = $ServerName
$this.ComputerSystem = Get-CimInstance Win32_ComputerSystem -ComputerName $ServerName
$this.OperatingSystem = Get-CimInstance Win32_OperatingSystem -ComputerName $ServerName
$this.Bios = Get-CimInstance -ClassName Win32_BIOS -ComputerName $ServerName
$this.NetworkAdapter = Get-CimInstance Win32_NetworkAdapterConfiguration -ComputerName $ServerName
}
}
Unfortunately, there is no convenient solution as of PowerShell 7.3.0, because properties in PowerShell custom classes cannot be implemented by way of accessor methods.
Adding this ability in a future version has been proposed in GitHub issue #2219 - years ago.
The following:
uses methods (e.g., .Bios() in lieu of properties (e.g. .Bios), so that the "property value" can be retrieved dynamically; the backing instance property, which is hidden with the hidden attribute, is a hashtable that maps "property names" to their values.
uses Start-ThreadJob, which ships with PowerShell (Core) 7+, and can be installed in Windows PowerShell with Install-Module ThreadJob, to asynchronously launch thread-based jobs on construction, which perform the Get-CimInstance calls in the background.
Class Server {
# Instance variables:
[string] $ServerName
# A hidden map (hashtable) that maps what are conceptually properties to the
# commands that retrieve their values, via thread jobs.
# Note that the static values defined here are *script blocks*, which are replaced
# with what their *invocation* evaluates to in the constructor.
# Note: By default, up to 5 thread jobs are permitted to run at a time.
# You can modify this limit with -ThrottleLimit on the first Start-ThreadJob call,
# which then applies to all subsequent Start-ThreadJob calls that do not themselves
# use -ThrottleLimit in the same session.
hidden [hashtable] $_jobsMap = #{
ComputerSystem = { Start-ThreadJob { Get-CimInstance Win32_ComputerSystem -ComputerName $using:ServerName } }
OperatingSystem = { Start-ThreadJob { Get-CimInstance Win32_OperatingSystem -ComputerName $using:ServerName } }
Bios = { Start-ThreadJob { Get-CimInstance -ClassName Win32_BIOS -ComputerName $using:ServerName } }
NetworkAdapter = { Start-ThreadJob { Get-CimInstance Win32_NetworkAdapterConfiguration -ComputerName $using:ServerName } }
}
# Constructor
Server([string] $ServerName) {
$this.ServerName = $ServerName
# Asynchronously start the thread jobs that populate the "property"
# values, i.e. the entries of the $this._jobsMap hashtable.
foreach ($key in #($this._jobsMap.Keys)) {
# Replace each predefined script block with the result of its
# *invocation*, i.e. with an object describing each launched thread job.
$this._jobsMap[$key] = & $this._jobsMap[$key]
}
}
# Methods that act like property accessors.
[object] ComputerSystem() {
return $this.get('ComputerSystem')
}
[object] OperatingSystem() {
return $this.get('OperatingSystem')
}
[object] Bios() {
return $this.get('Bios')
}
[object] NetworkAdapter() {
return $this.get('NetworkAdapter')
}
# Hidden helper method that returns the value of interest,
# making sure that a value has been received from the relevant
# thread job first.
hidden [object] get($propName) {
if ($this._jobsMap[$propName] -is [System.Management.Automation.Job]) {
# Note that any error-stream output from the jobs
# is *not* automatically passed through; -ErrorVariable is used
# to collect any error(s), which are translated into a script-terminating
# error with `throw`
$e = $null
$this._jobsMap[$propName] = $this._jobsMap[$propName] |
Receive-Job -Wait -AutoRemoveJob -ErrorVariable e
if ($e) {
throw $e[0]
}
}
return $this._jobsMap[$propName]
}
# Method that indicates whether *all* "properties" have finished
# initializing.
[bool] IsInitialized() {
$pendingJobs = $this._jobsMap.Values.Where({ $_ -is [System.Management.Automation.Job] })
return $pendingJobs.Count -eq 0 -or $pendingJobs.Where({ $_.State -in 'NotStarted', 'Running' }, 'First').Count -eq 0
}
}
Example use:
# Instantiate [Server], which asynchronously starts the thread jobs
# that will populate the properties.
$server = [Server]::new('.')
# Access a "property" by way of its helper method, which
# waits for the job to complete first, if necessary.
$server.Bios()
# Test if all property values have been retrieved yet.
$server.IsInitialized()

PowerShell Jobs, writing to a file

Having some problems getting a Start-Job script block to output to a file. The following three lines of code work without any problem:
$about_name = "C:\0\ps_about_name.txt"
$about = get-help about_* | select Name,Synopsis
if (-not (Test-
Path $about_name)) { ($about | select Name | sort Name | Out-String).replace("[Aa]bout_", "") > $about_name }
The file is created in C:\0\
But I need to do a lot of collections like this, so I naturally looked at stacking them in parallel as separate jobs. I followed online examples and so put the last line in the above as a script block invoked by Start-Job:
Start-Job { if (-not (Test-Path $about_name)) { { ($about | select Name | sort Name | Out-String).replace("[Aa]bout_", "") > $about_name } }
The Job is created, goes to status Running, and then to status Completed, but no file is created. Without Start-Job, all works, with Start-Job, nothing... I've tried a lot of variations on this but cannot get it to create the file. Can someone advise what I am doing wrong in this please?
IMO, the simplest way to get around this problem by use of the $using scope modifier.
$about_name = "C:\0\ps_about_name.txt"
$about = get-help about_* | select Name,Synopsis
$sb = { if (-not (Test-Path $using:about_name)) {
$using:about.Name -replace '^about_' | Sort-Object > $using:about_name
}
}
Start-Job -Scriptblock $sb
Explanation:
$using allows you to access local variables in a remote command. This is particularly useful when running Start-Job and Invoke-Command. The syntax is $using:localvariable.
This particular problem is a variable scope issue. Start-Job creates a background job with its own scope. When using -Scriptblock parameter, you are working within that scope. It does not know about variables defined in your current scope/session. Therefore, you must use a technique that will define the variable within the scope, pass in the variable's value, or access the local scope from the script block. You can read more about scopes at About_Scopes.
As an aside, character sets [] are not supported in the .NET .Replace() method. You need to switch to -replace to utilize those. I updated the code to perform the replace using -replace case-insensitively.
HCM's perfectly fine solution uses a technique that passes the value into the job's script block. By defining a parameter within the script block, you can pass a value into that parameter by use of -ArgumentList.
Another option is to just define your variables within the Start-Job script block.
$sb = { $about_name = "C:\0\ps_about_name.txt"
$about = get-help about_* | select Name,Synopsis
if (-not (Test-Path $about_name)) {
$about.Name -replace '^about_' | Sort-Object > $about_name
}
}
Start-Job -Scriptblock $sb
You've got to send your parameters to your job.
This does not work:
$file = "C:\temp\_mytest.txt"
start-job {"_" | out-file $file}
While this does:
$file = "C:\temp\_mytest.txt"
start-job -ArgumentList $file -scriptblock {
Param($file)
"_" | out-file $file
}

What's the fastest way to get online computers

I'm writing a function which returns all Online Computers in our network, so I can do stuff like this:
Get-OnlineComputers | % { get-process -computername $_ }
Now I basically got my function ready, but it's taking way too long.
I want to only return Computers which have WinRM active, but I also want to provide the option to get every computer even those which haven't got WinRM set up (switch parameter).
This is my function. first it creates a pssession to the domaincontroller, to get all computers in our LAN. then foreach computer, it will test if they have WinRM active or if they accept ping. if so, it gets returned.
$session = New-PSSession Domaincontroller
$computers = Invoke-Command -Session $session { Get-ADComputer -filter * } | select -ExpandProperty Name
$computers | % {
if ($IncludeNoWinRM.IsPresent)
{
$ErrorActionPreference = "SilentlyContinue"
$ping = Test-NetConnection $_
if ($ping.PingSucceeded -eq 'True')
{
$_
}
}
else
{
$ErrorActionPreference = "SilentlyContinue"
$WinRM = Test-WSMan $_
if ($WinRM)
{
$_
}
}
}
Is this the best way I can go to check my online computers? Does anyone have a faster and better idea?
Thanks!
Very Quick Solution is using the -Quiet Parameter of the Test-Connection cmdlet:
so for example:
$ping = Test-Connection "Computer" -Quiet -Count 1
if ($ping)
{
"Online"
}
else
{
"Offline"
}
if it's not enough fast for you, you can use the Send Method of the System.Net.NetworkInformation.Ping
here's a sample function:
Function Test-Ping
{
Param($computer = "127.0.0.1")
$ping = new-object System.Net.NetworkInformation.Ping
Try
{
[void]$ping.send($computer,1)
$Online = $true
}
Catch
{
$Online = $False
}
Return $Online
}
Regarding execute it on multiple computers, I suggest using RunSpaces, as it's the fastest Multithreading you can get with PowerShell,
For more information see:
Runspaces vs Jobs
Basic Runspaces implemenation
Boe Prox (master of runspaces) has written a function which is available from the Powershell Gallery. I've linked the script below.
He uses many of the answers already given to achieve the simultaneous examination of 100s of computers by name. The script gets WMI network information if test-connection succeeds. It should be fairly easy to adapt to get any other information you want, or just return the result of the test-connection.
The script actually uses runspace pools rather than straight runspaces to limit the amount of simultaneous threads that your loop can spawn.
Boe also wrote the PoSH-RSJob module already referenced. This script will achieve what you want in native PoSH without having to install his module.
https://gallery.technet.microsoft.com/scriptcenter/Speedy-Network-Information-5b1406fb

Synax Highlighting w/ Dynamic Scriptblock

I'm creating a dynamic ScriptBlock the way below so I can use local functions and variables and easily pass them to remote computers via Invoke-Command. The issue is that since all the text inside Create is enclosed with double quotes, I loose all my syntax highlighting since all editors see the code as one big string.
While this is only a cosmetic issue, I'd like to find a work around that allow my code to be passed without having double quotes. I've tried passing a variable inside Create instead of the actually text, but it does not get interpreted.
function local_admin($a, $b) {
([adsi]"WinNT://localhost/Administrators,group").Add("WinNT://$a/$b,user")
}
$SB = [ScriptBlock]::Create(#"
#Define Function
function local_admin {$Function:local_admin}
local_admin domain username
"#)
Invoke-Command -ComputerName server2 -ScriptBlock $SB
You can pass the function into the remote session using the following example. This allows you to define the ScriptBlock using curly braces instead of as a string.
# Define the function
function foo {
"bar";
}
$sb = {
# Import the function definition into the remote session
[void](New-Item -Path $args[0].PSPath -Value $args[0].Definition);
# Call the function
foo;
};
#(gi function:foo) | select *
Invoke-Command -ComputerName . -ScriptBlock $sb -ArgumentList (Get-Item -Path function:foo);
Here is a modified version of your function. Please take note that the domain and username can be dynamically passed into the remote ScriptBlock using the -ArgumentList parameter. I am using the $args automatic variable to pass objects into the ScriptBlock.
function local_admin($a, $b) {
([adsi]"WinNT://localhost/Administrators,group").Add("WinNT://$a/$b,user")
}
$SB = {
#Define Function
[void](New-Item -Path $args[0].PSPath -Value $args[0].Definition);
# Call the function
local_admin $args[1] $args[2];
}
Invoke-Command -ComputerName server2 -ScriptBlock $SB -ArgumentList (Get-Item -Path function:local_admin), 'domain', 'username';