# Update these variables to match your system
$appName='Microsoft System Center Virtual Machine Manager Agent (x64)'
$desiredVersion='10.22.1287.0'
$msiFile='X:\System Center Virtual Machine Manager\amd64\Setup\msi\Agent\vmmAgent.msi'
$maxWaitSeconds=120
$computersList = @'
    HYPERV001
    HYPERV002
'@

function installMsiOnRemoteComputer{
    param(
        $computernames=$env:computername,
        $msiFile,
        $destinationLocalTempFolder='C:\Temp',
        $testFileName='testfile.txt'
    )
    
    function translateLocalPathToSmbPath($computername,$localPath,$testFileName){
        $adminDriveLetter=[regex]::match($localPath,'^([\w\W])\:').captures.groups[1].value
        $partialPath=[regex]::match($localPath,'^([\w\W])\:(.*)').captures.groups[2].value
        $testPath=join-path "\\$computername\$adminDriveLetter`$" "$partialPath"
        if(!(test-path $testPath)){
            try{
                New-Item -Path $testPath -ItemType "directory" -force
            }catch{
                write-warning "Unable to create $testPath"
                return $false
            }
        }
        try{
            $null=New-Item -Path $testPath -Name $testFileName -ItemType "file" -force
            Remove-Item "$testPath\$testFileName" -force
            return $testPath
        }catch{
            write-warning "Unable to read or write to $testPath"
            return $false
        }        
    }

    $results=[hashtable]@{}
    $msiLocalFilePath=join-path $destinationLocalTempFolder $(split-path $msiFile -leaf)
    foreach ($computername in $computernames){
        $translatedDestination=translateLocalPathToSmbPath $computername $destinationLocalTempFolder $testFileName
        if($translatedDestination){
            copy-item $msiFile $translatedDestination
        }else{
            write-warning "Unable to copy $msiFile to $translatedDestination"
            $results+=[hashtable]@{$computername=$false}
            continue
        }        
        $psSession=new-psSession $computername
        if($psSession.State -eq 'Opened'){
            $result=invoke-command -session $pssession -scriptblock{
                param($filePath)
                $file=gi $filePath
                $DataStamp = get-date -Format yyyyMMddTHHmmss
                $logFile ="C:\" + '{0}-{1}.log' -f $file.name,$DataStamp
                $MSIArguments = @(
                    "/i"
                    ('"{0}"' -f $file.fullname)
                    "/qn"
                    "/norestart"
                    "/L*v"
                    $logFile
                )
                try{
                    [diagnostics.process]::start("msiexec.exe", $MSIArguments).WaitForExit()
                    write-host "MSIEXEC has been called for file $filePath on $env:computername"
                    return $true
                }catch{
                    write-warning $_
                    return $false
                }             
            } -Args $msiLocalFilePath

            # Note Error:
            # Resolved by not using -wait switch and calling [diagnostics.process] instead of Start-Process "msiexec.exe"
            # Although, this error would still being thrown with the [diagnostics.process] result of success
            # Processing data for a remote command failed with the following error message: The I/O operation has been aborted
            # because of either a thread exit or an application request. For more information, see the about_Remote_Troubleshooting
            # Help topic.
            #     + CategoryInfo          : OperationStopped: (:String) [], PSRemotingTransportException
            #     + FullyQualifiedErrorId : JobFailure
            #     + PSComputerName        : 

            $results+=[hashtable]@{$computername=$result}
            remove-psSession $psSession
        }else{
            write-warning "Unable to connect to $computername"
            $results+=[hashtable]@{$computername="Unable to connect to $computername"}
        }
    }
    return $results
}

function removeAppwizProgram($computernames=$env:computername,$appName='Firefox'){
    $results=[hashtable]@{}
    foreach($computer in $computernames){
        $session=new-pssession $computer
        if($session.State -eq 'Opened'){
            $result=invoke-command -session $session -scriptblock{
                param($appName)
                write-host "Checking $env:computername..."
                try{
                    # Method 1: try using the Uninstall method of the application packager
                    $app=Get-WmiObject -Class Win32_Product -Filter "Name='$appName'"
                    if($app.Name -eq $appName){
                        write-host "Uninstalling $app"
                        # pause
                        $null=$app.Uninstall()
                        $appStillExists=Get-WmiObject -Class Win32_Product -Filter "Name='$appName'"
                        if($appStillExists){
                            write-host "'$appName' still exists"
                            # Method 2: Using Registry
                            $uninstallStringRegPaths='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall','HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
                            $uninstallStrings=Get-ChildItem -Path $uninstallStringRegPaths
                            $uninstallString=($uninstallStrings|Get-ItemProperty|Where-Object {$_.DisplayName -match $appName}).UninstallString
                            if($uninstallString.count -eq 1){
                                $appCode=[regex]::match($uninstallString,'\{(.*)\}').Value
                                $uninstallCommand="& msiexec.exe /x $appCode /quiet /norestart"
                                write-host "Invoking uninstall Command: $uninstallCommand"
                                Invoke-Expression $uninstallCommand
                                $appStillExists=Get-WmiObject -Class Win32_Product -Filter "Name='$appName'"
                                if($appStillExists){
                                    write-warning "Uninstall has been unsuccessful at removing $appName"
                                    return $false
                                }else{
                                    return $true
                                }
                            }else{
                                write-warning "Please check this/these uninstall string(s):`r`n$uninstallString"
                                return $false
                            }
                        }else{
                            write-host "'$appName' has been removed"
                            return $true
                        }
                    }else{
                        write-host "No matches for $appName"
                        return $true
                    }                   
                }catch{
                    write-warning $_
                    return $false
                }
            } -Args $appName
            $results+=@{$computer=$result}
            remove-pssession $session
        }else{
            write-warning "Unable to connect to $computer"
            $results+=@{$computer=$null}
        }        
    }
    return $results
}

function sortArrayStringAsNumbers([string[]]$names){
    $hashTable=@{}
    $maxLength=($names | Measure-Object -Maximum -Property Length).Maximum
    foreach ($name in $names){
        #[int]$x=.{[void]($name -match '(?:.(\d+))+$');$matches[1]}
        #$x=.{[void]($name -match '(?:.(\d+)+)$');@($name.substring(0,$name.length-$matches[1].length),$matches[1])}
        $originalName=$name
        $x=.{Clear-Variable matches
            [void]($name -match '(?:.(\d+)+)\w{0,}$');
            if($matches){
                [int]$trailingNonDigits=([regex]::match($name,'\D+$').value).length
                if($trailingNonDigits){
                    $name=$name.substring(0,$name.length-$trailingNonDigits)
                }
                return ($name.substring(0,$name.length-$matches[1].length))+$matches[1].PadLeft($maxLength,'0');
            }else{
                return $name+''.PadLeft($maxLength,'0');
            }}
        $hashTable.Add($originalName,$x)
        }
    $sorted=foreach($item in $hashTable.GetEnumerator() | Sort Value){$item.Name}
    return $sorted
}

function main{
    $computerNames=sortArrayStringAsNumbers(@($computersList -split "`n" -replace "\..*$")|%{$_.tostring().trim()})
    $results=[hashtable]@{}
    foreach($node in $computernames){
        write-host "Processing $node ..."
        $appVersionPassed=invoke-command -computername $node {
            param($appName,$desiredVersion)
            $matchedApp=Get-WmiObject -Class Win32_Product -Filter "Name='$appName'"
            if($matchedApp.Version -eq $desiredVersion){
                return $true
            }else{
                return $false
            }
        } -Args $appName,$desiredVersion
        
        if(!$appVersionPassed){
            $vcredist140Installed=invoke-command -computername $node {        
                $null=choco install vcredist140 -y
                $chocoApps=choco list -l
                if($chocoApps -match 'vcredist140'){
                    return $true
                }else{
                    return $false
                }
            }
            if($vcredist140Installed){
                $appRemoved=removeAppwizProgram $node $appName
                if(($appRemoved|out-string) -match "True"){
                    try{
                        installMsiOnRemoteComputer $node $msiFile
                    }catch{
                        write-warning $_                    
                    }
                    write-host "Now waiting up to $maxWaitSeconds seconds before checking on the install result"
                    $timer=[System.Diagnostics.Stopwatch]::StartNew()
                    do{                        
                        start-sleep -seconds 5
                        $appVersionPassed=invoke-command -computername $node {
                            param($appName,$desiredVersion)
                            $matchedApp=Get-WmiObject -Class Win32_Product -Filter "Name='$appName'"
                            if($matchedApp.Version -eq $desiredVersion){
                                return $true
                            }else{
                                return $false
                            }
                        } -Args $appName,$desiredVersion
                        if($appVersionPassed){
                            write-host "$appName $desiredVersion installed on $node successfully"
                            $results+=[hashtable]@{$node='$appName $desiredVersion installed'}                            
                        }
                        $exitCondition=$timer.elapsed.totalseconds -ge $maxWaitSeconds 
                    }until($appVersionPassed -or $exitCondition)
                    if(!$appVersionPassed){                        
                            write-host "$appName $desiredVersion has NOT been installed successfully on $node"
                            $results+=[hashtable]@{$node='$appName $desiredVersion NOT installed'}
                    }
                    $timer.stop()
                }else{
                    write-warning "Unable to uninstall outdated app on $node"
                    $results+=[hashtable]@{$node='Unable to uninstall outdated app'}
                }
            }else{
                $results+=[hashtable]@{$node='Unable to install vcredist140'}
            }
        }else{
            write-host "$appName is already at $desiredVersion"
            $results+=[hashtable]@{$node='$appName is already at $desiredVersion'}
        }
    }
    return sortArrayStringAsNumbers($results.GetEnumerator()|select Name,Value)
}

main