# installMSiRemoteComputers.ps1
# version 0.0.1

$computernames='REMOTEPC001','REMOTEPC002'
$thisMsiFile='C:\Temp\something.msi'
$appName='testapp'
$desiredVersion='1.0'
$maxWaitSeconds=120

function installMsiOnRemoteComputers($computernames,$msiFile,$appName,$desiredVersion,$maxWaitSeconds){
    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
    }
    $results=[hashtable]@{}
    foreach($computername in $computernames){
        try{
            installMsiOnRemoteComputer $computername $msiFile
            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 $computername {
                    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 $computername successfully"
                    $results+=[hashtable]@{$computername='$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 $computername"
                    $results+=[hashtable]@{$computername="$appName $desiredVersion NOT installed"}
            }
            $timer.stop()
        }catch{
            write-warning $_
            $results+=[hashtable]@{$computername="$_"}
            exit 1   
        }
    }
    return $results
}

installMsiOnRemoteComputers $computernames $thisMsiFile $appName $desiredVersion $maxWaitSeconds