Powershell has a lot of great tools for running remote commands and lately I have been putting them to the test. One command that has given me a spot of frustration is the Invoke-Command cmdlet. In fact I started using Invoke-Expression with winrs to get around some of the thornier problems. Turns out I needed to RTFM and and get back on the Invoke-Command train and here's why.
Invoke-Expression is used to execute whatever string is passed to it. This makes it very simple to run commands that don't have Powershell equivalents. I started using it to run the 7zip command line executable when I needed to perform operations on zip files from within a script. Powershell does have some native ways to deal with zip files, but I find them to be unnecessarily cumbersome. 7zip is fairly simple and straightforward, so until Powershell has a Unzip-Files cmdlet, I am going to keep using this one. Since it will execute any string you pass to it, you can pass the winrs command and perform work remotely. This does work fine, but it crops up with two issues. First, it relies on winrs to perform the actual commands remotely, rather than using the built-in remote cmdlets in Powershell. The second issue is that you have to construct the string prior to running the Invoke-Expression cmdlets, which means an extra line of code and an extra variable. That's really not a big deal, but it's not as elegant. Some Powershell aficionados really try to cram the most into a single line with pipes, to the point of complete unread-ability. I am not that bad, but less lines is better to me.
Invoke-Command can take a file, script block, or input object and execute it remotely or locally. One of the parameters is Computername, so it's pretty obvious that this was meant to take advantage of the remote command capabilities available in Powershell. One thing to keep in mind is that the script block will be sent to the remote computer without being evaluated. That means any local variable references will be lost. The way around this is to add a param($arg1,$arg2) line to the script block and then use the parameter ArgumentList to submit local variables to the command. It's very much like writing a mini function or script in the block, which is probably why it's called a scriptblock. Powershell is pretty literal like that.
This all came to a head because I was using Invoke-Expression to call winrs to call a sqlpackage command remotely to update a database with dacpacs. The sqlpackage executable takes up a sizable amount of memory and winrs was throwing either an OutOfMemory exception or StackOverflow exception depending on how I called it. I started digging into the properties of the WinRM object to increase shell and plug-in max memory, but soon abandoned that line of attack. This was a script that needed to run in multiple environments, and I didn't want to add the pre-requisite of changing WinRM settings in order for it to run. After a long weekend I decided to give Invoke-Command a go. Wouldn't you know it, the command ran without issue when I did it directly within a Powershell session. But when I tried to construct the command in a script it was failing with null value exceptions. What the heck?
Like I said before, this was a classic case of RTFM (or in this case read the freakin' technet page). I was trying to pass a script block with local variables like this:
Invoke-Command -ComputerName $srv.Name -Scriptblock {& $srv.filePath $releasenumber}
The $srv and $releasenumber variables didn't exist on the remote computer, so naturally it was returning null value exceptions. As usual, Powershell was doing exactly what I asked it to. I just didn't understand what I was asking clearly. After reading up on the command a little, I realized I should be doing it like this:
Invoke-Command -ComputerName $srv.Name -Scriptblock {param($srv, $releasenumber) & $srv.filePath $releasenumber} -ArgumentList $srv,$releasenumber
The param command lets the Invoke-Command cmdlet know that it needs to substitute the values in argument list for the variables in the script block. Just like if you called a script with parameters.
Lesson learned? Powershell will only do what you ask it to, so you should probably know what you are asking.
Invoke-Command technet page is here. Check out example 9.
More on running executables in Powershell here.Labels: powershell, Scripts, winrm, winrs