As is the norm for a typical day, I was working on one thing when I was distracted by a shiny rabbit hole (to mix some metaphors). Half a day later I have a new PowerShell function that not only might you find useful, but I think it has some nice scripting features you might want to try yourself.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
The problem I had was to remotely query a Windows system and determine what versions of PowerShell were installed. For servers, you can use Get-WindowsFeature to check if PowerShell-V2 is installed. But what about PowerShell Core, PowerShell 7, or even a preview build? My solution was to query the registry.
Windows PowerShell uses different keys and property names than PowerShell Core, which includes PowerShell 7. Once I knew where to look, I could write a function. Because you can only query the registry on the "local" machine, my function would need to use PowerShell remoting. Here's the code, which is up on Github.
The function includes comment-based help.
I wrote the function to accept computer names and credentials or an existing PSSession object. However, under the hood I'm only using PSSessions. If computer names are used, I create a temporary PSSession and add it to an array.
Process { if ($PSCmdlet.ParameterSetName -eq 'computer') { foreach ($computer in $Computername ) { $PSBoundParameters["Computername"] = $Computer #create a session Write-Verbose "[$(New-TimeSpan -start $start) PROCESS] Creating a temporary PSSession to $($computer.toUpper())" If ($Credential.username) { Write-Verbose "[$(New-TimeSpan -start $start) PROCESS] Using credential for $($credential.username)" } Try { #save each created session to $tmp so it can be removed at the end $all += New-PSSession @PSBoundParameters -ErrorAction Stop -OutVariable +tmp } Catch { Write-Error $_ } } #foreach computer } #if computer parameterset
Note that when I create the session, I am saving it $tmp using Out-Variable. This is so that at the end of the command I can remove any sessions that I created. If a PSSession is passed, I add it to the array.
foreach ($sess in $session) { if ($sess.state -eq 'opened') { Write-Verbose "[$(New-TimeSpan -start $start) PROCESS] Using session for $($sess.computername.toUpper())" $all +=$sess } }
All of the sessions are added to an array of parameters which I splat to Invoke-Command.
Invoke-Command @icmParams
One thing that is different than what you might expect, is that this statement happens in the function's End scriptblock. This is because I want to rely on PowerShell to best manage all the connections. That's why I include the ThrottleLimit parameter which gets passed along to Invoke-Command. In earlier versions, I was piping PSSessions and running Invoke-Command in the Process scriptblock. This meant sessions were being processed one at a time. By moving the command to the End scriptblock, total processing time was cut almost in half. To summarize this point: the Process scriptblock is used for building a list of PSSessions. That's it.
The Begin scriptblock is where I define the scriptblock to run remotely. I'm a big fan of using Write-Verbose in my commands. The potential obstacle though, is that if I run the function with -Verbose, when the scriptblock runs remotely, it doesn't know anything about my verbose preferences. Here's how I handle that.
In the scriptblock, I add a parameter to indicate my Verbose preference.
$sb = { param([string]$VerbPref = "SilentlyContinue") $VerbosePreference = $VerbPref
In my function, I added a value to -Argumentlist that shows the verbose preference on my side.
$icmParams = @{ Scriptblock = $sb Argumentlist = $VerbosePreference HideComputerName = $True ThrottleLimit = $ThrottleLimit ErrorAction = "Stop" Session = $null }
The last part of the verbose feature is that I'm using a running time as part of the message. I also indicate where in the function the statement is happening. In the scriptblock, my verbose messages employ $using to get the $start variable which is defined on my computer. I also use REMOTE to indicate the verbose message is happening elsewhere.
Write-Verbose "[$(New-TimeSpan -start $using:start) REMOTE ] Querying \\$($env:computername)\$pskey"
Here's what it all looks like.
Now I can tell what is happening and where. Note that I aligned the leading portion of the verbose message. I find this makes it much easier to read.
As for the result: nice and simple.
Or by specifying computer names.
I have to say I'm pleased with the results. I hope you'll give it a try and let me know what you think. It should work in both Windows PowerShell and PowerShell 7.
I like your solution.
Regarding PScore. PS7 replaces previous installations but not completely and registry has leftovers like 7.0.0-rc.3 or -preview which are not actually installed.
That is good to know. In my tests, the registry is accurately reflecting what is installed. But yiou make a good point. This data is only as good as much as you trust it.
This does not show side by side builds such as daily build or preview build that are not installed using the MSI. IIRC, the registry is only updated via MSI’s. Just tested and this script does not find 7.1 preview or build of the day not installed via MSI
This function is only as good as you trust your registry. Or your users.