I’ve seen what I’m about to show you done repeatedly in Windows PowerShell. With that thought in my mind, I’ve decided it’s time to officially write about it. The first time I noticed someone doing it, well, it was me. I did, however, write a correction to my team so they knew I was aware of my error, and as a hope they wouldn’t repeat it.
Those doing this may be new to PowerShell, but if you aren’t careful and don’t fully understand the capabilities of the cmdlets you use, then it’s possible you might make this mistake, too. What am I talking about? I’m talking about Invoke-Command
and an improper way that it’s sometimes used.
Invoke-Command
‘s purpose is to run a command, or commands, on remote computers and return the results back to the local computer. That said, it can be used on the local computer, as well; however, this is usually only necessary to check if PowerShell Remoting is working on the local computer. While I don’t normally do this in my examples, I’m piping my result of this example to Format-Table -AutoSize
once they’re returned from the remote computer. This is so it’ll display better on this webpage.
Invoke-Command -Computer DC01 -ScriptBlock {Get-PSDrive -PSProvider FileSystem} | Format-Table -AutoSize
Name Used (GB) Free (GB) Provider Root CurrentLocation PSComputerName ---- --------- --------- -------- ---- --------------- -------------- A A:\ DC01 C 12.91 26.75 C:\ Users\tommymaynard\Documents DC01 D D:\ DC01
This example of Invoke-Command
connected to the remote computer DC01, ran a filtered Get-PSDrive
command, returned the results to my computer, and then were formatted by the Format-Table
cmdlet.
Now, let’s say we have… 23 computer names stored in the variable $Computers
and we want to run the command we used above, against each computer. What so many people seem to do is wrap Invoke-Command
in a Foreach
construct (think: loop). I’m guessing, because I’ve made the error myself, that it’s because someone learned about Foreach
first. Like I said, I discovered my error on my own, and only moments later. It didn’t wake me at 2 a.m. in a cold sweat after two months, which could’ve been possible.
Here’s how not to do this, and again, $Computers
is holding the names of 23 computers.
Foreach ($Computer in $Computers) { Invoke-Command -ComputerName $Computer -ScriptBlock {Get-PSDrive -PSProvider FileSystem} }
Name Used (GB) Free (GB) Provider Root CurrentLocation PSComputerName ---- --------- --------- -------- ---- --------------- -------------- C 20.47 59.18 C:\ ...rd\Documents DC02 D D:\ DC02 A A:\ WEB01 C 50.36 29.30 C:\ ...rd\Documents WEB01 D D:\ WEB01 A A:\ SQL01 C 53.59 26.06 C:\ ...rd\Documents SQL01 D D:\ SQL01 ... # Not all results included.
What we’ve done here is force Invoke-Command
to run against only one computer at a time. During each iteration of the Foreach
loop, Invoke-Command
runs against the computer name currently stored in $Computer
. This variable is updated to the next computer name in the variable at the start of each loop. It’s like this: connect to DC02 and run the command. Now, connect to WEB01 and run the command. Are you done, Invoke-Command
? Okay then, now Foreach
says to run the same command against the computer SQL01. That’s not how this cmdlet was designed to be used.
Before we go any further, I wrapped this command inside the Measure-Command
cmdlet to determine how long it took my system to run this against the computers in $Computers
. It took nearly 30 seconds. While that’s not an eternity, we’re only talking about 23 computers, and we’re only talking about a single command.
Measure-Command -Expression { Foreach ($Computer in $Computers) { Invoke-Command -ComputerName $Computer -ScriptBlock {Get-PSDrive -PSProvider FileSystem} } }
Days : 0 Hours : 0 Minutes : 0 Seconds : 27 Milliseconds : 513 Ticks : 275132689 TotalDays : 0.000318440612268519 TotalHours : 0.00764257469444444 TotalMinutes : 0.458554481666667 TotalSeconds : 27.5132689 TotalMilliseconds : 27513.2689
Now, let’s set this command up the proper way. The ComputerName parameter of Invoke-Command
can take a collection of computers. It should be said that many cmdlets can take more than one computer as a value to their ComputerName parameter. The difference is that Invoke-Command
can run against all of the 23 computers at the same time. In fact, the default throttle limit—the number of computers in which the command will run against simultaneously—is 32. It can be changed by including the ThrottleLimit parameter, such as -ThrottleLimit 50
or -ThrottleLimit 15
.
Now, here’s how this command should’ve been written.
Invoke-Command -ComputerName $Computers -ScriptBlock {Get-PSDrive -PSProvider FileSystem}
Name Used (GB) Free (GB) Provider Root CurrentLocation PSComputerName ---- --------- -------u-- -------- ---- --------------- -------------- A A:\ DC02 A A:\ WEB01 C 41.72 37.93 C:\ ...rd\Documents WEB01 C 41.13 38.52 C:\ ...rd\Documents DC02 D D:\ DC02 D D:\ WEB01 A A:\ SQL01 C 46.75 32.91 C:\ ...rd\Documents SQL01 D D:\ SQL01 ... # Not all results included.
I measured this command five times, and the average of those five runs was only 2.8 seconds. Remember, the Foreach
loop took almost 30 seconds to get the same results. This is due to the fact that, again, Invoke-Command
will work with the remote computers simultaneously (when it’s not inside a Foreach
). There’s never any waiting to run the command on any additional computers unless there’s more computers than the value of ThrottleLimit.
Please keep the topic in this post in mind, as it’s possible to momentarily forget this feature just long enough to wrap the Invoke-Command
cmdlet in a Foreach
, or other looping construct.