PowerShell: Multi-threading, Progress Bars and GUI Input, Part 3

Before we dive in, if you’re just joining us now on this third installment, here’s part one and here’s part two to get you started.

So let’s start off with some apologies. No, it’s not for being a lazy engineer and not writing a blog in forever, it’s an apology for being a lazy engineer and leaving out a cmdlet that is extremely useful and not double-checking my variables. In the last post, I dropped this While loop:

 

While ((Test-Connection -ComputerName $comp -Quiet) -eq $False)
{
#Blank Space to kill time during while loop
}
Yeah, lazy Russ. Instead of that blank space, if you want it to kill time and wait so it’s not constantly testing a connection, or really if you just want a script to sit still for a period of time, use Start-Sleep. Go ahead, type get-help start-sleep and read through. It’s pretty self-explanatory, but a lot of people don’t know it exists.

The second part of that apology has to do with my variables. I put out this line for starting jobs:

start-job -Name $srv -ScriptBlock {param([string]$srv)PINGServerTrue $srv} -InitializationScript $initscriptTrue -ArgumentList $srv |Out-Null
and then right behind it said that I’m dropping it into a foreach loop of:

foreach ($serv in $serverlist)
See the problem here? Yeah, I didn’t when I first wrote it, and now I’m seeing it glaring at me. In the start-job block, I’m using $srv as the variable, even though I’m OBVIOUSLY using $serv as my variable. Let’s fix that:

start-job -Name $serv -ScriptBlock {param([string]$serv)PINGServerTrue $serv} -InitializationScript $initscriptTrue -ArgumentList $serv |Out-Null
That feels better. It’s these little things that you catch when you start running your scripts to test, and up to that point, I hadn’t run this one yet. Please put away the pitchforks.

Progress Bars with PowerShell

Digging into things, let’s do as I promised and delve into progress bars. Progress bars are a wonderful and wacky thing that we have all learned to take for granted. Seriously, think about it. Every time you install, copy, paste, or really do anything that takes any amount of time, there’s a progress bar. If it’s not a straight up bar, it’s some sort of progress indicator, whether it be an hourglass or a spinny circle thing. The whole purpose of these things is to let you, the user, know that something is actually happening. We’ve all installed some piece of software or run some command where you kick it off and nothing appears to happen. What do we do when that happens? We get cranky, we start clicking things, sometimes executing the same piece of software three or four times, and then we get upset when we get thirty messages saying that the installation is already running.

Or is that just me? I know I’m not the first person to break a keyboard in frustration ….

What were we talking about? Oh yeah! Progress bars. Microsoft built in a cmdlet called Write-Progress, that, of all things, writes your progress. At its roots, you call the cmdlet, and through proper syntax it will reflect specific indicators that you tell it to. Time for a code drop and an explanation:

$totalNumber = 1000
$numcount = 0
foreach ($number in 1..$totalNumber )
{
$numcount++
#Hey look! It’s a progress bar!
Write-Progress  -Activity “Doing things that can only be expressed by a totally awesome progress bar” -status “I’m counting Numbers!” -currentoperation “$numcount numbers counted” -Percentcomplete ($numcount / $totalnumber * 100)

write-host $number
}
Here I’ve got a very basic example of how to make this thing work. First, I’ve defined a parameter of a specific value. Since this is numerical, it makes life a little easier, and we’ll get to that in just a moment. Then I’ve got what I like to call a counting variable. To make sure it always starts at 0, I set it to 0 before my foreach loop. Next is the foreach loop. Before you write in and tell me that I should be using the full cmdlet name instead of an alias, yes, yes I should, but at least I didn’t write it as %. For those who don’t know what I’m talking about, foreach is an alias of the cmdlet ForEach-Object. If you don’t know what aliases are, let me know.

The foreach loop should be 1 – 10000. The number that it happens to be on in that set is going to be passed through the loop in the $number variable. Just inside the foreach loop, I’m iterating that $numcount variable. This is the running count of how far into my number set I am.

Breaking Down the Write-Progress Cmdlet

We see that I’m defining an attribute called “Activity”. This is generally where you put in what it is that your status bar is displaying. The “Status” attribute is, well, the status. As you can see, it’s counting numbers, not mixing a cocktail, climbing a mountain, or doing jumping jacks. What’s that? You want me to be more specific? Okay. Fine. Activity would be: “Please wait while we’re installing this awesome software”, and Status would be “Checking for components”, “Creating folder Hierarchy”, or “Waiting for services to start”. Does that make more sense? I hope so.

Next, we’ve got “Current Operation”. Wait a second, I thought we just stated what we were doing, why do we need a Current Operation? That’s what I thought at first too. The answer is Granularity. Take this, for example: your script is scanning folders for a specific .dll or something similar, so Write-Progress -Activity “Installation in progress, please do not close window” -Status “Scanning for installed components” -CurrentOperation “$folder is currently being scanned”. In one progress bar window, we’re notifying you of not only the big picture of what are we doing, but we’re providing granularity of operation. Do you have to use all these? Nope. Do I? Heck yeah!

Now we’ve got the notification part out of the way, what about the actual bar itself? That’s the Percentcomplete attribute. For my little example, I’ve got $numcount, which is getting one added to it every time we pop through the loop, I’ve got $totalnumber, which is, well, the total number, and we’ve got * 100.

Let’s walk through the logic. Time for a word problem: What percent of 1000 is 130? To get that answer, divide 130 by 1000 and then multiply by 100. It’s 13. You didn’t really need to do that math. For that Percentcomplete attribute, you just have to have some way to tell it what percent completion you’re at.  Put it all together (or just copy that script blob up there into a PowerShell script) and you get something like this:

powershell progress barsA few paragraphs up, I said that I’d talk about how having a straight up number for that $totalnumber variable was much easier. Extrapolate all this out into a scenario where you are iterating through a list of servers. How do you know how many servers there are in that list? Well, if you said you call the Count attribute of the variable, then you’re absolutely right. Enter more script blob!

#Clear Screen
cls
#Set Variables
$serverlist =@()
$serverlist += @(TextBox “Server List” “Enter the list of Servers to test connection to:”)

#Instantiating variable to use to count servers for use in progress bar. This ensures it always starts at 0
$srvcount = 0

#Instantiating the $serv variable ahead of time ’cause things didn’t seem to work right if you didn’t.
$serv = $null

#How long to pause in while loop waiting for threads to complete
$SleepTimer = 500

#How long to wait for all jobs to complete. This prevents it from hanging and never finishing.
$MaxWaitAtEnd = 60000

#How many will run at the same time?
$MaxThreads = 10

#Set Start Time
$ProcStartTime = Get-Date
#Inform user that existing jobs are stopping and then stop all existing jobs
“Killing existing jobs . . .”
Get-Job | Remove-Job -Force
“Done.”
“Waiting for servers to go down”
foreach ($serv in $serverlist)
{
#increment count of Servers that it has started
$srvcount++

#Check if number of running jobs is greater than number of threads defined
#If it is greater, stay in this while loop until it drops below defined threshold
While ($(Get-Job -state running).count -ge $MaxThreads)
{
#Hey look! It’s a progress bar!
-Progress  -Activity “Doing things that can only be expressed by a totally awesome progress bar” -status “Waiting for threads to close” -currentoperation “$srvcount servers started – $($(Get-Job -state running).count) threads currently open” -Percentcomplete ($srvcount / $Serverlist.Count * 100)
Start-Sleep -Milliseconds $SleepTimer
}

If you walk on down through that dump, you’ll see things are starting to come together a little more coherently, we’re defining starting values for some of the variables that are going to get used in different loops, a couple places where I’m doing some checks against other variables, etc. I’ll be revisiting some of these parts in the next part of our series, but the primary thing I wanted you to see is down there towards the end, $serverlist.count. Since I’m feeding my foreach loop a serverlist, I’m using the .count attribute to calculate my Percentcomplete off of.

Next time we’ll be diving into creating the actual jobs, and depending on how long winded I get on that part, we may just see this thing really come together into a fully working script. As always, any questions, comments or snide remarks, reach out!

If you have questions about getting some scripting done for your organization, send us an email or give us a call at 502-240-0404!