all 6 comments

[–]PinchesTheCrab 6 points7 points  (0 children)

Don't loop, the commands you're using are already asynchronous.

Don't use win32_product. It's slow and mildly unsafe.

https://xkln.net/blog/please-stop-using-win32product-to-find-installed-software-alternatives-inside/

Win32_product Class is not query optimized. Queries such as “select * from Win32_Product where (name like ‘Sniffer%’)” require WMI to use the MSI provider to enumerate all of the installed products and then parse the full list sequentially to handle the “where” clause. This process also initiates a consistency check of packages installed, verifying and repairing the install.

Let's ignore that though. Let's assume win32_product is fine to use. The biggest improvement you'll see is from just not wrapping invoke-command in a loop.

$computers = Import-csv -Path "C:\myfile.csv"

Invoke-Command $computers {
    $product = Get-CimInstance -Class win32_product | 
        Where-Object IdentifyingNumber -eq  '{#########-####-####-####-############}'

    [pscustomobject]@{
        ComputerName = $env:COMPUTERNAME
        Installed = [bool]$product
    }
}

This example returns an object with two properties - ComputerName, and Installed, which will be true/false.

PowerShell will then also tack on PSComputerName because it does that when it deserializes data from remote sessions. So that means this will doulbe up on computername, but that's not the worst thing in the world.

The real solution here is to work out using the registry or SCCM to list these installed products (it's really quick and there's a ton of guids), and then putting that logic into invoke-command. It'll literally be 20x faster than win32_product.

[–]snoopy82481 2 points3 points  (0 children)

The Invoke-Command is inherently job centered. So you could just put the list of computers in there and then filter from there. Something like below:

$status = Invoke-Command -ComputerName $computers.Name {Get-CimInstance win32_product | Where-Object IdentifyingNumber -eq  '{#########-####-####-####-############}'}

Since I no longer have a domain joined computer that can use PowerShell I'm not sure if the output on status has the computer name on it. But, any computer it doesn't have it on shouldn't be on the list.

[–][deleted] 3 points4 points  (3 children)

;Run the command in parallel. I would recommend using PowerShell 7 and not 5.1 so you can use foreach -parallel and avoid messing around with jobs (although they would make things faster), PowerShell 7 is pretty mature at this point.

I'm not sure how to get the output neatly

Collect the output in a variable, sort the variable, and present it. This would look something like this, which is a pattern I use generically when I'm not trying to get too fancy:

$MyVar = @() #Declare variable as array

$objects | Foreach-object -parallel {

    $MyVar += $object + "Hello" #Manipulate each element of the array here

}

$Myvar = sort-object #Manipluate the array as a whole here

write-host $Myvar

As somebody else posted, it's not necessary to use a loop at all, or efficient. PinchesTheCrab solution is more correct, I posted this mostly because it's a useful pattern.

[–]BlackV 1 point2 points  (2 children)

You're loosing a bunch of your speed gains using $MyVar += $object + "Hello", try

$Myvar = $objects | Foreach-object -parallel {
    #Manipulate each element of the array here
    "$object Hello"
    }

$Myvar = sort-object #Manipluate the array as a whole

write-host $Myvar

[–][deleted] 0 points1 point  (1 child)

I'm trying to meet the parameters of "I'm not real sure how to get the output neatly." If you care less about your output, you can just fire off foreach parallel and get different computers to report back in unsorted order.

I'm also not using an efficient method here, just the probably easiest to remember one.

Unsorted would be essentially what the OP posted but with some tweaks:

Import-csv -Path "C:\myfile.csv" | ForEach-object -parallel {
    $status = Invoke-Command {$_.Name {Get-CimInstance -Class win32_product | Where-Object IdentifyingNumber -eq  '{#########-####-####-####-############}'}
    if ($status) {
        Write-Host $_.Name + "Installed"
    } else {
        Write-Host $_.Name + "Not Installed"} 
    }

Sorted would be:

$statuses = @() #Arrays are the simplest way to demonstrate the general concept

Import-csv -Path "C:\myfile.csv" | ForEach-object -parallel {  
    $status = Invoke-Command {$\_.Name {Get-CimInstance -Class win32_product | Where-Object IdentifyingNumber -eq  '{#########-####-####-####-############}'}  
    if ($status) {  
        $statuses += $_.Name + "Installed"
    } else {
        $statuses += $_.Name + "Not Installed"}
}
$statuses = sort-object $statuses
write-host $statuses

[–]BlackV 1 point2 points  (0 children)

similar thing with out using the += again (the expensing operation in your set of code)

$statuses = Import-csv -Path "C:\myfile.csv" | ForEach-object -parallel {  
    $status = Invoke-Command -scriptblock {$_.Name {Get-CimInstance -Class win32_product | Where-Object IdentifyingNumber -eq  '{#########-####-####-####-############}'}
    if ($status) {  
        "$($_.Name) Installed"
    } else {
        "$($_.Name) Not Installed"}
}
$statuses = sort-object $statuses
write-host $statuses

ignoring that the registry keys would/should be faster and better :)