Powershell Asynchronous Function Call - powershell

I'm trying to create some sort of Service Script that notifies me when a specific computer in the network is online. To achieve this, I'd like to have a function that runs in the background and checks every x seconds whether the computer is reachable (through an ICMP package) or not.
Pseudo code:
class Computer{
[bool]$IsOnline
[ScriptBlock]$InvokeFunction = {
Notify()
}
}
$collectionOfComputers = #()
function Invoke-Worker{
while($true){
foreach($computer in $collectionOfComputers){
ping $computer
if($computer.IsOnline){
$computer.InvokeFunction()
}
}
Start-Sleep -Seconds 10
}
}
I hope what I'm trying to do is achievable with powershell and the question is okay to be asked.

Basic structure but I think meets all of the requirements you mentioned and some you don't:
$Status = #{}
while ($true)
{
## By continually retrieving the contents of the file, will allow you to update the input on the fly and have the script dynamically adjust
$Computers = Get-Content 'computers.txt'
if ($Computers.Count -gt 0)
{
foreach ($Computer in $Computers)
{
if (-not($Status[$Computer]))
{
$Status[$Computer] = [pscustomobject]#{
Name = $Computer
IsOnline = $false
StateChange = $null
}
}
## If IsOnline NOW
if (Test-Connection $Computer -Count 1 -Quiet)
{
## If IsOnline NOW compared to the state on the last poll
if ($Status[$Computer].IsOnline)
{
$Status[$Computer].StateChange = $false
}
else
{
$Status[$Computer].StateChange = $true
}
## Set the new online state for our object
$Status[$Computer].IsOnline = $true
}
else
{
## If IsOnline NOW compared to the state on the last poll
if ($Status[$Computer].IsOnline)
{
$Status[$Computer].StateChange = $true
}
else
{
$Status[$Computer].StateChange = $false
}
## Set the new online state for our object
$Status[$Computer].IsOnline = $false
}
}
}
$Status
Start-Sleep -Seconds 10
}
Variant: Producer/Consumer Parallelism -- without the Producer (still have to poll the output object to retrieve the results, even though the icmp polling is happening in a different runspace)
## Adapted from Lee Holmes Producer/Consumer Parallelism here: https://www.leeholmes.com/blog/2018/09/05/producer-consumer-parallelism-in-powershell/
$parallelScript = {
param(
## The output buffer to write responses to
$OutputQueue,
## State tracking, to help threads communicate
## how much progress they've made
$ThreadId, $ShouldExit
)
## Continually work until
## the 'ShouldExit' flag is set
$Status = #{}
$workItem = $null
while(! $ShouldExit.Value)
{
## By continually retrieving the contents of the file, will allow you to update the input on the fly and have the script dynamically adjust
$Computers = Get-Content 'C:\users\user\Desktop\computers.txt'
if ($Computers.Count -gt 0)
{
foreach ($Computer in $Computers)
{
if (-not($Status[$Computer]))
{
$Status[$Computer] = [pscustomobject]#{
Name = $Computer
IsOnline = $false
StateChange = $null
}
}
## If IsOnline NOW
if (Test-Connection $Computer -Count 1 -Quiet)
{
## If IsOnline NOW compared to the state on the last poll
if ($Status[$Computer].IsOnline)
{
$Status[$Computer].StateChange = $false
}
else
{
$Status[$Computer].StateChange = $true
}
## Set the new online state for our object
$Status[$Computer].IsOnline = $true
}
else
{
## If IsOnline NOW compared to the state on the last poll
if ($Status[$Computer].IsOnline)
{
$Status[$Computer].StateChange = $true
}
else
{
$Status[$Computer].StateChange = $false
}
## Set the new online state for our object
$Status[$Computer].IsOnline = $false
}
}
}
Start-Sleep -Seconds 10
## Add the result to the output queue
$OutputQueue.Enqueue($Status)
}
else
{
## If there was no work, wait a bit for more.
Start-Sleep -m 100
}
}
## Create a set of background PowerShell instances to do work, based on the
## number of available processors.
$threads = 1
$runspaces = 1..$threads | Foreach-Object { [PowerShell]::Create() }
$outputProgress = New-Object 'Int[]' $threads
$outputQueue = New-Object 'System.Collections.Concurrent.ConcurrentQueue[Object]'
$shouldExit = $false
## Spin up each of our PowerShell runspaces. Once invoked, these are actively
## waiting for work and consuming once available.
for($counter = 0; $counter -lt $threads; $counter++)
{
$null = $runspaces[$counter].AddScript($parallelScript).
AddParameter("OutputQueue", $outputQueue).
AddParameter("ThreadId", $counter).
AddParameter("ShouldExit", [ref] $shouldExit).BeginInvoke()
}
## Wait for our worker threads to complete processing the
## work.
try
{
do
{
## If there were any results, output them.
$scriptOutput = $null
while($outputQueue.TryDequeue([ref] $scriptOutput))
{
$scriptOutput
}
Start-Sleep -m 100
## See if we still have any busy runspaces. If not, exit the loop.
$busyRunspaces = $runspaces | Where-Object { $_.InvocationStateInfo.State -ne 'Complete' }
} while($busyRunspaces)
}
finally
{
## Clean up our PowerShell instances
foreach($runspace in $runspaces)
{
$runspace.Stop()
$runspace.Dispose()
}
}

You need to pass the collection of computers to invoke-worker. When you pass the variable to a function you will get a copy of $collectionOfComputers then you can use start-job.
function Invoke-Worker($collectionOfComputers){
while($true){
foreach($computer in $collectionOfComputers){
ping $computer
if($computer.IsOnline){
$computer.InvokeFunction()
}
}
Start-Sleep -Seconds 10
}
}
The class Computer must be part of the job.

Related

Coding with powershell

I'm new with powershell and i would like to use a loop to ping several Printers on my network.
My problem is : once i'm in the loop of pinging , i can't go out of the loop ...
I tried several things from google but without success ( start-stop , Timer ) . Does anybody have any idea?
Here is the code :
$BtnStartPingClicked = {
if ($LblFileSelectPing.Text -eq "*.txt") {
Add-Type -AssemblyName PresentationCore,PresentationFramework
$ButtonType = [System.Windows.MessageBoxButton]::OK
$MessageIcon = [System.Windows.MessageBoxImage]::Error
$MessageBody = "Please select a list of printer first"
$MessageTitle = "Error"
$Result = [System.Windows.MessageBox]::Show($MessageBody,$MessageTitle,$ButtonType,$MessageIcon)
Write-Host "Your choice is $Result"
}
else {
do {
$IPList = Get-Content ($LblFileSelectPing.Text)
$snmp = New-Object -ComObject olePrn.OleSNMP
$ping = New-Object System.Net.NetworkInformation.Ping
$i = 11
$j = 1
foreach ($Printer in $IPList) {
try {
$result = $ping.Send($Printer)
} catch {
$result = $null
}
if ($result.Status -eq 'Success') {
$((Get-Variable -name ("GBMachine"+$j+"Ping")).value).Visible = $True
$j++
test-Connection -ComputerName $Printer -Count 1 -Quiet
$printerip = $result.Address.ToString()
# OPEN SNMP CONNECTION TO PRINTER
$snmp.open($Printer, 'public', 2, 3000)
# MODEL
try {
$model = $snmp.Get('.1.3.6.1.2.1.25.3.2.1.3.1')
} catch {
$model = $null
}
# Serial
try {
$serial = $snmp.Get('.1.3.6.1.4.1.1602.1.2.1.8.1.3.1.1').toupper()
} catch {
$Dns = $null
}
# progress
$TBMonitoringPing.SelectionColor = "green"
$TBMonitoringPing.AppendText("$Printer is Pinging")
$TBMonitoringPing.AppendText("`n")
$mac = (arp -a $Printer | Select-String '([0-9a-f]{2}-){5}[0-9a-f]{2}').Matches.Value
# OPEN SNMP CONNECTION TO PRINTER
$((Get-Variable -name ('LblMach' + $i)).value).Text = "IP : $Printerip"
$i++
$((Get-Variable -name ('LblMach' + $i)).value).Text = "Model : $Model"
$i++
$((Get-Variable -name ('LblMach' + $i)).value).Text = "MAC : $mac"
$i++
$((Get-Variable -name ('LblMach' + $i)).value).Text = "Serial : $serial"
$TBAnswerMachine.AppendText("$Model")
$TBAnswerMachine.AppendText("`n")
$TBAnswerMachine.AppendText("$Printer - $Serial")
$TBAnswerMachine.AppendText("`n")
$TBAnswerMachine.AppendText("$Mac")
$TBAnswerMachine.AppendText("`n")
$TBAnswerMachine.AppendText("`n")
Get-Content ($LblFileSelectPing.Text) | Where-Object {$_ -notmatch $Printer} | Set-Content ("C:\_canonsoftware\out.txt")
$i = $i+7
$snmp.Close()
Start-Sleep -milliseconds 1000 # Take a breather!
}
else {
$TBMonitoringPing.selectioncolor = "red"
$TBMonitoringPing.AppendText("$Printer not pinging")
$TBMonitoringPing.AppendText("`n")
Start-Sleep -milliseconds 1000 # Take a breather!
}
}
$LblFileSelectPing.Text = "C:\_canonsoftware\out.txt"
} until($infinity)
}
}
thanks for your answers...
1 - part of my object are indeed not declared in the code because they are in my other PS1 file....
2 - I do a do until infinity because i don't want to stop the code before i decide it...
3 - I didn't explain my problem correctly ( excuse my poor english ) ... i would like to be able to go out of the loop do until at the moment i click on a stop button ... but apprently the windows doens't respond while in the loop ... i have to stop the script with powershell ... which is annoying because i'd like to make an executable with it ... and not have to go out of my program ...
thank you for your ideas

How to ping continuously in the background in powershell?

This is my first program in powershell, Im trying to get from the user input and then pinging the IP address or the hostname, Creating text file on the desktop.
But if the user wants the add more than one IP I get into infinite loop.
Here Im asking for IP address.
$dirPath = "C:\Users\$env:UserName\Desktop"
function getUserInput()
{
$ipsArray = #()
$response = 'y'
while($response -ne 'n')
{
$choice = Read-Host '
======================================================================
======================================================================
Please enter HOSTNAME or IP Address, enter n to stop adding'
$ipsArray += $choice
$response = Read-Host 'Do you want to add more? (y\n)'
}
ForEach($ip in $ipsArray)
{
createFile($ip)
startPing($ip)
}
}
Then I creating the file for each IP address:
function createFile($ip)
{
$textPath = "$($dirPath)\$($ip).txt"
if(!(Test-Path -Path $textPath))
{
New-Item -Path $dirPath -Name "$ip.txt" -ItemType "file"
}
}
And now you can see the problem, Because I want the write with TIME format, I have problem with the ForEach loop, When I start to ping, And I cant reach the next element in the array until I stop
the cmd.exe.
function startPing($ip)
{
ping.exe $ip -t | foreach {"{0} - {1}" -f (Get-Date), $_
} >> $dirPath\$ip.txt
}
Maybe I should create other files ForEach IP address and pass params?
Here's a old script I have. You can watch a list of computers in a window.
# pinger.ps1
# example: pinger yahoo.com
# pinger c001,c002,c003
# $list = cat list.txt; pinger $list
param ($hostnames)
#$pingcmd = 'test-netconnection -port 515'
$pingcmd = 'test-connection'
$sleeptime = 1
$sawup = #{}
$sawdown = #{}
foreach ($hostname in $hostnames) {
$sawup[$hostname] = $false
$sawdown[$hostname] = $false
}
#$sawup = 0
#$sawdown = 0
while ($true) {
# if (invoke-expression "$pingcmd $($hostname)") {
foreach ($hostname in $hostnames) {
if (& $pingcmd -count 1 $hostname -ea 0) {
if (! $sawup[$hostname]) {
echo "$([console]::beep(500,300))$hostname is up $(get-date)"
$sawup[$hostname] = $true
$sawdown[$hostname] = $false
}
} else {
if (! $sawdown[$hostname]) {
echo "$([console]::beep(500,300))$hostname is down $(get-date)"
$sawdown[$hostname] = $true
$sawup[$hostname] = $false
}
}
}
sleep $sleeptime
}
pinger microsoft.com,yahoo.com
microsoft.com is down 11/08/2020 17:54:54
yahoo.com is up 11/08/2020 17:54:55
Have a look at PowerShell Jobs. Note that there are better and faster alternatives (like thread jobs, runspaces, etc), but for a beginner, this would be the easiest way. Basically, it starts a new PowerShell process.
A very simple example:
function startPing($ip) {
Start-Job -ScriptBlock {
param ($Address, $Path)
ping.exe $Address -t | foreach {"{0} - {1}" -f (Get-Date), $_ } >> $Path
} -ArgumentList $ip, $dirPath\$ip.txt
}
This simplified example does not take care of stopping the jobs. So depending on what behavior you want, you should look that up.
Also, note there there is also PowerShell's equivalent to ping, Test-Connection

multithread with runspaces instead of foreach cycle

I have a script with a foreach cycle. It has about a dozen functions, each collecting information from remote machines' C$ share (cutting text files, checking file version, etc.)
This is however taking some time, since each machine's data collected after one by one. (sometimes it runs with 500+ input)
Wish to put this into runspaces with parallel execution, but so far no examples worked. I am quite new to the concept.
Current script's outline
$inputfile = c:\temp\computerlist.txt
function 1
function 2
function 3, etc
foreach cycle
function 1
function 2
function 3
All results written to screen with write-host for now.
This example pings a number of server in parallel, so you easily can modify it for your demands:
Add-Type -AssemblyName System.Collections
$GH = [hashtable]::Synchronized(#{})
[System.Collections.Generic.List[PSObject]]$GH.results = #()
[System.Collections.Generic.List[string]]$GH.servers = #('server1','server2','server3');
[System.Collections.Generic.List[string]]$GH.functions = #('Check-Server');
[System.Collections.Generic.List[PSObject]]$jobs = #()
#-----------------------------------------------------------------
function Check-Server {
#-----------------------------------------------------------------
# a function which runs parallel
param(
[string]$server
)
$result = Test-Connection $server -Count 1 -Quiet
$GH.results.Add( [PSObject]#{ 'Server' = $server; 'Result' = $result } )
}
#-----------------------------------------------------------------
function Create-InitialSessionState {
#-----------------------------------------------------------------
param(
[System.Collections.Generic.List[string]]$functionNameList
)
# Setting up an initial session state object
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
foreach( $functionName in $functionNameList ) {
# Getting the function definition for the functions to add
$functionDefinition = Get-Content function:\$functionName
$functionEntry = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $functionName, $functionDefinition
# And add it to the iss object
[void]$initialSessionState.Commands.Add($functionEntry)
}
return $initialSessionState
}
#-----------------------------------------------------------------
function Create-RunspacePool {
#-----------------------------------------------------------------
param(
[InitialSessionState]$initialSessionState
)
$runspacePool = [RunspaceFactory]::CreateRunspacePool(1, ([int]$env:NUMBER_OF_PROCESSORS + 1), $initialSessionState, $Host)
$runspacePool.ApartmentState = 'MTA'
$runspacePool.ThreadOptions = "ReuseThread"
[void]$runspacePool.Open()
return $runspacePool
}
#-----------------------------------------------------------------
function Release-Runspaces {
#-----------------------------------------------------------------
$runspaces = Get-Runspace | Where { $_.Id -gt 1 }
foreach( $runspace in $runspaces ) {
try{
[void]$runspace.Close()
[void]$runspace.Dispose()
}
catch {
}
}
}
$initialSessionState = Create-InitialSessionState -functionNameList $GH.functions
$runspacePool = Create-RunspacePool -initialSessionState $initialSessionState
foreach ($server in $GH.servers)
{
Write-Host $server
$job = [System.Management.Automation.PowerShell]::Create($initialSessionState)
$job.RunspacePool = $runspacePool
$scriptBlock = { param ( [hashtable]$GH, [string]$server ); Check-Server -server $server }
[void]$job.AddScript( $scriptBlock ).AddArgument( $GH ).AddArgument( $server )
$jobs += New-Object PSObject -Property #{
RunNum = $jobCounter++
JobObj = $job
Result = $job.BeginInvoke() }
do {
Sleep -Seconds 1
} while( $runspacePool.GetAvailableRunspaces() -lt 1 )
}
Do {
Sleep -Seconds 1
} While( $jobs.Result.IsCompleted -contains $false)
$GH.results
Release-Runspaces | Out-Null
[void]$runspacePool.Close()
[void]$runspacePool.Dispose()
This would be concurrent and run in about 10 seconds total. The computer could be localhost three times if you got it working.
invoke-command comp1,comp2,comp3 { sleep 10; 'done' }
Simple attempt at api (threads):
$a = [PowerShell]::Create().AddScript{sleep 5;'a done'}
$b = [PowerShell]::Create().AddScript{sleep 5;'b done'}
$c = [PowerShell]::Create().AddScript{sleep 5;'c done'}
$r1 = $a.BeginInvoke(); $r2 = $b.BeginInvoke() $r3 = $c.BeginInvoke()
$a.EndInvoke($r1); $b.EndInvoke($r2); $c.EndInvoke($r3)
a done
b done
c done

Only prints the last server in the list, I want all servers

This only prints the last server in the list, I'm looking to get all servers and print to screen
$machines = (Get-BrokerMachine -AdminAddress $adminaddress -DesktopGroupName $deliverygroup | Select-Object DNSname).DNSname
foreach($machine in $machines){
$machinelist = Get-BrokerMachine -HostedMachineName $machine
if($machinelist.InMaintenanceMode -eq $true){
$status = "$machine is in maintenance mode"
}else {
$status = "$machine is not in maintenance mode"
}
}
Write-Host $status
Here is a more PowerShell-like approach (not tested):
Get-BrokerMachine -AdminAddress $adminaddress -DesktopGroupName $deliverygroup | ForEach-Object {
$machineName = $_.DNSName
[PSCustomObject] #{
"MachineName" = $machineName
"MaintenanceMode" = (Get-BrokerMachine -HostedMachineName $machine).InMaintenanceMode
}
} | Export-Csv "C:\whatever\results.csv" -NoTypeInformation
$Status is constantly being overwritten by the current machine in your list.
You're looking for:
$Status+=
As opposed to:
$Status=
You'll also want to explicitly state that $Status will be an array at the beginning like so:
$Status=#()
Or when you create the variable and omit the line at the beginning.
[array]$Status +=
Otherwise, you'll get results that run together as it will be treated as a [String]
another funky mode :
function get-BrokerMachineMode
{
param (
[Parameter(Mandatory = $true)]
[string[]]$machines
)
begin
{
$ErrorActionPreference = 'Stop'
Add-Type -Language CSharp #"
public class BrokenBroker {
qpublic System.String MachineName;
public System.String MaintenanceMode;
public BrokenBroker (string MachineName, string MaintenanceMode)
{
this.MachineName = MachineName;
this.MaintenanceMode = IsInMaintenanceMode;
}
}
"#
$status = #()
Write-Verbose "Created objects..."
}
process
{
try
{
$machines = (Get-BrokerMachine -AdminAddress $adminaddress `
-DesktopGroupName $deliverygroup | Select-Object DNSname).DNSname
foreach ($machine in $machines)
{
Write-Verbose "Checking machine: $machine"
$machinelist = Get-BrokerMachine -HostedMachineName $machine
if ($machinelist.InMaintenanceMode -eq $true)
{
$status += New-Object BrokenBroker($machine, $true)
}
else
{
$status += New-Object BrokenBroker($machine, $false)
}
}
}
catch
{
Write-Error $error[0].Exception.Message
}
$status
}
end
{
Write-Verbose "Done"
}
}
this is a function you just must to load then you can launch it just by using this command:
$computers = get-content = {PATH TO TXT FILE}
$list = get-BrokerMachineMode -machines $computers -Verbose

Check for net.tcp binding in PowerShell

I am configuring a process that checks IIS settings on a Web Server for net.tcp bindings for a particual Web Site, and if it does not exist, create it. I have this chunk of code to check
$Websites = Get-ChildItem IIS:\Sites
foreach ($Site in $Websites) {
if ($Site.name -eq "LOSSI") {
$Binding = $Site.bindings
foreach ($bind in $Binding.collection) {
if ($bind -eq "net.tcp 443:*")
{
Write-Host $bind
}
}
}
}
But I never fall into the last conditional. I have validated by hand that the binding is set to
LOSSI
3
Started
D:\LOSSI
http *:63211: net.tcp 443:
I imagine I am doing something silly wrong, but I cannot figure it out. Is there an easier way to check a website for tcp binding?
function Test-TcpPort {
<#
.SYNOPSIS
Determine if computers have the specified ports open.
.EXAMPLE
PS C:\> Test-TcpPort -ComputerName web01,sql01,dc01 -Port 5985,5986,80,8080,443
.NOTE
Example function from PowerShell Deep Dives 2013.
#>
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[Alias("CN","Server","__Server","IPAddress")]
[string[]]$ComputerName = $env:COMPUTERNAME,
[int[]]$Port = 23,
[int]$Timeout = 5000
)
Process {
foreach ($computer in $ComputerName) {
foreach ($p in $port) {
Write-Verbose ("Checking port {0} on {1}" -f $computer, $p)
$tcpClient = New-Object System.Net.Sockets.TCPClient
$async = $tcpClient.BeginConnect($computer, $p, $null, $null)
$wait = $async.AsyncWaitHandle.WaitOne($TimeOut, $false)
if(-not $Wait) {
[PSCustomObject]#{
Computername = $ComputerName
Port = $P
State = 'Closed'
Notes = 'Connection timed out'
}
} else {
try {
$tcpClient.EndConnect($async)
[PSCustomObject]#{
Computername = $computer
Port = $p
State = 'Open'
Notes = $null
}
} catch {
[PSCustomObject]#{
Computername = $computer
Port = $p
State = 'Closed'
Notes = ("{0}" -f $_.Exception.Message)
}
}
}
}
}
}
}
Microsoft reference script for check port
add this function in
for powershell 64 bit
C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1
for powershell 32 bit
C:\Windows\SysWOW64\WindowsPowerShell\v1.0\profile.ps1
then open powershell use this
Test-TCPPort google.com -Port 80
output :
True