# getWindowsResourceUtilization.ps1
# version 0.02
# Gather information on a list of Windows Machines
# This version will test RCP or WinRM access from localhost toward each destination
# If RPC is unavailable, then WinRM would be tried
# If neither procotol is reachable from localhost toward destination(s), then such machine scanning will be skipped

$computernames=@(
    'LAX-RDSNODE01',
    'LAX-RDSNODE02',
    'LAX-RDSNODE03',
    'LAX-RDSNODE04'
)

function checkWindows($computernames=$env:computername){

    function checkPort($computername=$env:computername,$port=135,$protocol='tcp',$verbose=$false){
        function includePortQry{
            if (!(Get-Command portqry.exe -ErrorAction SilentlyContinue)){
                if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
                Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))}
                choco install portqry -y
                if (Get-Command portqry.exe -ErrorAction SilentlyContinue){return $true}else{return $false}
            }else{
                return $true
            }
        }
        
        $portQryExists=includePortQry
        if($portQryExists){        
            $ip=[System.Net.Dns]::GetHostAddresses($computername).IPAddressToString|select-object -first 1
            $reachable=if($port -ne 135){[bool](portqry -n $ip -p $protocol -e $port|find ': LISTENING')
                }else{
                    [bool](portqry -n $ip -p $protocol -e $port|find 'Total endpoints found: ')
                }       
            if($verbose){write-host "$port/$protocol : $reachable"}
            $result=[PSCustomObject]@{
                source=$env:computername
                destination=$computername
                port=$port
                protocol=$protocol
                reachable=$reachable
            }
        }else{
            write-warning 'Unable to proceed without PortQry'
            return $null
        }
        return $result
    }
    function checkComputer($computername=$env:computername){
        function checkPendingReboot([string]$computer=$ENV:computername){ 
            function checkRegistry{
                if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { return $true }
                if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress" -EA Ignore) { return $true }
                if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { return $true }
                if (Get-Item "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackagesPending" -EA Ignore) { return $true }
                if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting" -EA Ignore) { return $true }
                if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { return $true }
                if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations2 -EA Ignore) { return $true }
                if (Get-Item "HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttempts" -EA Ignore) { return $true }
                try{ 
                    $ccmUtil=[wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
                    $status=$ccmUtil.DetermineIfRebootPending()
                    if(($null -ne $status) -and $status.RebootPending){
                        #$result.SCCMRebootPending = $true
                        return $true
                    }
                }catch{}
            }
            
            function checkRegistryRemotely{ # need to QA this snippet
                param(
                    $remoteComputer,
                    $registryHive=[Microsoft.Win32.RegistryHive]::LocalMachine,
                    $registryPath,
                    $keyName
                )
                $remoteComputer='???'
                $registryHive=[Microsoft.Win32.RegistryHive]::LocalMachine
                $registryPath='Software\Microsoft\Windows\CurrentVersion\Component Based Servicing'
                $keyName='Version'

                $regHive=[Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($registryHive,$remoteComputer)
                $key=$regHive.OpenSubKey($registryPath)
                $regHive.GetValue($keyName)
            }

            if ($ENV:computername -eq $computer){
                $result=checkRegistry;
            }else{
                $result=Invoke-Command -computername $computer -ScriptBlock{
                        param($importedFunc);
                        [ScriptBlock]::Create($importedFunc).Invoke();
                    } -ArgumentList ${function:checkRegistry}
            }
            return $result;
        }

        function getSessionsInfo([string[]]$computername=$env:computername){
            $results=@()
            function getDisconnectedSessionInfo($thisLine,$computer){
                # convert multiple spaces into single space and split pieces into array
                $sessionArray = $thisLine.Trim() -Replace '\s+',' ' -Split '\s'
                $properties = @{
                    UserName = $sessionArray[0]
                    ComputerName = $computer
                    }
                $properties.SessionName = $null
                $properties.SessionId = $sessionArray[1]
                $properties.State = $sessionArray[2]                        
                $properties.IdleMinutes=.{
                        [string]$x=$sessionArray[3]
                        switch -regex ($x){
                            '\.' {0;break}                   
                            '\+' {$dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
                                    $hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
                                    $dayMinutes+$hourMinutes
                                    break;
                                    }               
                            '\:' {try{
                                    ([TimeSpan]::Parse($x)).totalMinutes
                                    }catch{
                                        "Invalid value: $x"
                                        }
                                    break
                                    }
                            default {$x}
                        }                                  
                    }
                $properties.LogonTime = $sessionArray[4..6] -join ' '
                $result=New-Object -TypeName PSCustomObject -Property $properties
                return $result
                }
                    
            function getActiveSessionInfo($thisLine,$computer){
                $sessionArray = $thisLine.Trim() -Replace '\s+',' ' -Split '\s'
                $properties = @{
                    UserName = $sessionArray[0]
                    ComputerName = $computer
                    }
                $properties.SessionName = $sessionArray[1]
                $properties.SessionId = $sessionArray[2]
                $properties.State = $sessionArray[3]
                $properties.IdleMinutes=.{
                        $x=$sessionArray[4]
                        switch -regex ($x){
                            '\.' {0;break}                   
                            '\+' {  $dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
                                    $hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
                                    $dayMinutes+$hourMinutes
                                    break;
                                    }               
                            '\:' {  $timeFragments=[regex]::match($x,'(\d+)\:(\d+)')
                                    [Int16]$hours=$timeFragments.Groups[1].Value
                                    [Int16]$minutes=$timeFragments.Groups[2].Value
                                    $minutes + ($hours * 60)
                                    break
                                    }
                            default {$x}
                        }
                    }
                $properties.LogonTime = $sessionArray[5..($sessionArray.GetUpperBound(0))] -join ' '
                $result=New-Object -TypeName PSCustomObject -Property $properties
                return $result
                }
        
            foreach ($computer in $computername){
                try {
                    # Perusing legacy commandlets as there are no PowerShell equivalents at this time
                    $sessions=quser /server:$computer 2>&1 | Select-Object -Skip 1
                    ForEach ($line in $sessions) {               
                        $fragments = $line.Trim() -Replace '\s+',' ' -Split '\s'
                        $disconnectedSession=$fragments[2] -eq 'Disc' -or $fragments[3] -eq 'Disc'
                        if ($disconnectedSession){
                            $result=getDisconnectedSessionInfo $line $computer
                        }else{
                            $result=getActiveSessionInfo $line $computer
                        }
                        if($result){$results+=$result}
                    }
                }catch{
                    $results+=New-Object -TypeName PSCustomObject -Property @{
                        ComputerName=$computer
                        Error=$_.Exception.Message
                    } | Select-Object -Property UserName,ComputerName,SessionName,SessionId,State,IdleMinutes,LogonTime,Error|sort -Property UserName
                }
            }
            return $results
        }

        $excludeServiceNames='conhost|dasHost|dllhost|ctfmon|audiodg|cytray|ApplicationFrameHost|SYSTEM|LOCAL SERVICE|NETWORK SERVICE|DWM-\d+|UMFD-\d+'
        $cpuObject = Get-WmiObject -computername $computername win32_processor -ea stop |select Name,NumberOfLogicalProcessors,LoadPercentage
        $cpuCount=($cpuObject|  Measure-Object -property NumberOfLogicalProcessors -Sum).Sum
        $cpuLoad = ($cpuObject|  Measure-Object -property LoadPercentage -Average | Select @{Name="CurrentLoad";Expression={"{0:N2} %" -f ($_.Average)}}).CurrentLoad
        $osAndMemory = gwmi -Class win32_operatingsystem -computername $computername -ea stop| select @{Name="os";Expression={$_.Caption}},@{Name="Memory";Expression={"{0:N2} GB" -f ($_.TotalVisibleMemorySize / 1048576)}},@{Name = "Utilization"; Expression = {"{0:N2} %" -f ((($_.TotalVisibleMemorySize - $_.FreePhysicalMemory)*100)/ $_.TotalVisibleMemorySize) }}
        if($osAndMemory){
            $os=$osAndMemory.os;
            $memory=$osAndMemory.Memory;
            $memoryUtilization=$osAndMemory.Utilization;
            }
        $volumes=gwmi -Class win32_volume -computername $computername -Filter "DriveType!=5" -ea stop| `
                    ?{$_.DriveLetter -ne $isnull}|  `
                        Select-object @{Name="Letter";Expression={$_.DriveLetter}}, `
                            @{Name="Label";Expression={$_.Label}},@{Name="Capacity";Expression={"{0:N2} GiB" -f ($_.Capacity/1073741824)}}, `
                            @{Name = "Utilization"; Expression = {"{0:N2} %" -f  ((($_.Capacity-$_.FreeSpace) / $_.Capacity)*100) }}| `
                        Sort-Object -Property Letter
        $topProcesses=get-process -computername $computername -EA Ignore|Group-Object -Property ProcessName | `
                        Select-Object -First 10 | `
                        Select-Object Name, @{N='Memory (MB)';E={[math]::Round((($_.Group|Measure-Object WorkingSet -Sum).Sum/1MB),2)}} | `
                        Sort-Object -Property "Memory (MB)" -Descending| `
                        ?{$_.Name -ne '' -and $_.Name -notmatch $excludeServiceNames}
        $sessions=getSessionsInfo $computername
        $memoryUsagePerUser=get-wmiobject win32_process -computername $computername| `
                            select-object @{N='User';E={$_.getowner().user}}, WorkingSetSize  | Group-Object user | `
                            where-object {$_.Name -ne '' -and $_.Name -notmatch $excludeServiceNames} | `
                            select-object Name, 
                                @{N='Memory (MB)';E={[math]::Round(($_.Group.WorkingSetSize  | Measure-Object -Sum).Sum / 1Mb,2)  }}, 
                                @{N='Status';E={$x=$_.Name;$($sessions|?{$_.UserName -eq $x}).State }},
                                @{N='IdleMinutes';E={$x=$_.Name;$($sessions|?{$_.UserName -eq $x}).IdleMinutes }} | `
                            sort-object -property "Memory (MB)" -Descending
        # $activeUsers=(getSessionsInfo $computername|?{$_.State -eq 'Active'}).UserName
        $lastUpdates=get-hotfix|select HotfixID,Description,InstalledOn|?{$_.installedon -gt (get-date).addDays(-30)}
        $appWiz=try{
            Get-Package -Provider Programs -IncludeWindowsInstaller|select-object Name,Version|sort -property Name
        }catch{
            Get-WmiObject -Class Win32_Product|select-object Name,Version|sort -property Name
        }
        $lastbootTime=(Get-CimInstance -ClassName win32_operatingsystem -computername $computername).lastbootuptime.toString().trim()
        #Preempt this error:
        #The term 'Get-Package' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the
        #spelling of the name, or if a path was included, verify that the path is correct and try again.
        return [PSCustomObject]@{
            computername=$computername
            os=$os
            cpuCount=$cpuCount
            cpuLoad=$cpuLoad
            ramCapacity=$memory
            ramUtilization= $memoryUtilization
            volumes=($volumes|out-string).trim()
            topProcesses=($topProcesses|out-string).trim()
            memoryUsagePerUser=($memoryUsagePerUser|out-string).trim()
            #activeUsers=($activeUsers|out-string).trim()
            installedApps=($appWiz|out-string).trim()
            lastUpdates=($lastUpdates|out-string).trim()
            pendingReboot=if(checkPendingReboot){$true}else{$false}
            lastBootupTime=$lastbootTime
        }
    }
    $results=@()
    foreach ($computer in $computernames){
        try{
            write-host "checking $computer..."
            #$rpcPortOpen=if($env:computername -in @($computer,"$computer.$env:userdnsdomain")){$true}else{(checkport $computer 135).reachable}
            $rpcPortOpen=if($env:computername -in @($computer,"$computer.$env:userdnsdomain")){
                    $true
                }else{
                    test-netconnection $computer -port 135 -informationlevel quiet
                    }           
            if($rpcPortOpen){
                $result=checkComputer $computer
                $results+=$result
            #}elseif((checkport $computer 5985).reachable){
            }elseif(test-netconnection $computer -port 5985 -informationlevel quiet){    
                $session=new-pssession $computer
                if($session.State -eq 'Opened'){
                    $result=invoke-command -session $session -scriptblock{
                        param($checkcomputer)
                        [scriptblock]::create($checkcomputer).invoke()
                    } -Args ${function:checkComputer}|select-object * -ExcludeProperty pscomputername,runspaceid,PSShowComputerName
                    $results+=$result
                    remove-pssession $session
                }else{
                    write-warning "$env:computername cannot initiate a WinRM session toward $computer"
                }                
            }else{
                write-warning "$env:computername is not able to reach $computer via RPC nor WinRM"
            }
        }catch{
            write-host $_            
        }
    }
    return $results
}

# enter-pssession DYNAMICS-CRM -SessionOption $(new-pssessionoption -IncludePortInSPN)
$systemUtilization=checkWindows $computernames
$systemUtilization |select-object computername,cpuCount,cpuLoad,ramCapacity,ramUtilization,activeUsers,volumes|ft -Wrap 
# getWindowsResourceUtilization.ps1
# Gather information on a list of Windows Machines
# Requires WMI Ports Access:
# 135/TCP
# 49152-65535/TCP (RPC dynamic ports – Windows Vista, 2008 and above)
# 1024-65535/TCP (RPC dynamic ports – Windows NT4, Windows 2000, Windows 2003)
# 445/TCP - Windows Performance Counter Access (SMB, RPC/NP)

$computernames=@(
    'LAX-RDSNODE01',
    'LAX-RDSNODE02',
    'LAX-RDSNODE03',
    'LAX-RDSNODE04'
)

function checkWindows($computernames=$env:computername){
    function checkComputer($computername=$env:computername){
        $excludeServiceNames='conhost|dasHost|dllhost|ctfmon|audiodg|cytray|ApplicationFrameHost|SYSTEM|LOCAL SERVICE|NETWORK SERVICE|DWM-\d+|UMFD-\d+'
        $cpuObject = Get-WmiObject -computername $computername win32_processor -ea stop |select Name,NumberOfLogicalProcessors,LoadPercentage
        $cpuCount=($cpuObject|  Measure-Object -property NumberOfLogicalProcessors -Sum).Sum
        $cpuLoad = ($cpuObject|  Measure-Object -property LoadPercentage -Average | Select @{Name="CurrentLoad";Expression={"{0:N2} %" -f ($_.Average)}}).CurrentLoad
        $osAndMemory = gwmi -Class win32_operatingsystem -computername $computername -ea stop| select @{Name="os";Expression={$_.Caption}},@{Name="Memory";Expression={"{0:N2} GB" -f ($_.TotalVisibleMemorySize / 1048576)}},@{Name = "Utilization"; Expression = {"{0:N2} %" -f ((($_.TotalVisibleMemorySize - $_.FreePhysicalMemory)*100)/ $_.TotalVisibleMemorySize) }}
        if($osAndMemory){
            $os=$osAndMemory.os;
            $memory=$osAndMemory.Memory;
            $memoryUtilization=$osAndMemory.Utilization;
            }
        $volumes=gwmi -Class win32_volume -computername $computername -Filter "DriveType!=5" -ea stop| `
                    ?{$_.DriveLetter -ne $isnull}|  `
                        Select-object @{Name="Letter";Expression={$_.DriveLetter}}, `
                            @{Name="Label";Expression={$_.Label}},@{Name="Capacity";Expression={"{0:N2} GiB" -f ($_.Capacity/1073741824)}}, `
                            @{Name = "Utilization"; Expression = {"{0:N2} %" -f  ((($_.Capacity-$_.FreeSpace) / $_.Capacity)*100) }}| `
                        Sort-Object -Property Letter
        $topProcesses=get-process -computername $computername|Group-Object -Property ProcessName | `
                        Select-Object -First 10 | `
                        Select-Object Name, @{N='Memory (MB)';E={[math]::Round((($_.Group|Measure-Object WorkingSet -Sum).Sum/1MB),2)}} | `
                        Sort-Object -Property "Memory (MB)" -Descending| `
                        ?{$_.Name -ne '' -and $_.Name -notmatch $excludeServiceNames}
        $memoryUsagePerUser=get-wmiobject win32_process -computername $computername| `
                            select-object @{N='User';E={$_.getowner().user}}, WorkingSetSize  | Group-Object user | `
                            select-object Name, @{N='Memory (MB)';E={[math]::Round(($_.Group.WorkingSetSize  | Measure-Object -Sum).Sum / 1Mb,2)  }} | `
                            ?{$_.Name -ne '' -and $_.Name -notmatch $excludeServiceNames} | sort-object -property "Memory (MB)" -Descending
        
        function getSessionsInfo([string[]]$computername=$env:computername){
            $results=@()
            function getDisconnectedSessionInfo($thisLine,$computer){
                # convert multiple spaces into single space and split pieces into array
                $sessionArray = $thisLine.Trim() -Replace '\s+',' ' -Split '\s'
                $properties = @{
                    UserName = $sessionArray[0]
                    ComputerName = $computer
                    }
                $properties.SessionName = $null
                $properties.SessionId = $sessionArray[1]
                $properties.State = $sessionArray[2]                        
                $properties.IdleMinutes=.{
                        [string]$x=$sessionArray[3]
                        switch -regex ($x){
                            '\.' {0;break}                   
                            '\+' {$dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
                                    $hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
                                    $dayMinutes+$hourMinutes
                                    break;
                                    }               
                            '\:' {try{
                                    ([TimeSpan]::Parse($x)).totalMinutes
                                    }catch{
                                        "Invalid value: $x"
                                        }
                                    break
                                    }
                            default {$x}
                        }                                  
                    }
                $properties.LogonTime = $sessionArray[4..6] -join ' '
                $result=New-Object -TypeName PSCustomObject -Property $properties
                return $result
                }
                    
            function getActiveSessionInfo($thisLine,$computer){
                $sessionArray = $thisLine.Trim() -Replace '\s+',' ' -Split '\s'
                $properties = @{
                    UserName = $sessionArray[0]
                    ComputerName = $computer
                    }
                $properties.SessionName = $sessionArray[1]
                $properties.SessionId = $sessionArray[2]
                $properties.State = $sessionArray[3]
                $properties.IdleMinutes=.{
                        $x=$sessionArray[4]
                        switch -regex ($x){
                            '\.' {0;break}                   
                            '\+' {  $dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
                                    $hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
                                    $dayMinutes+$hourMinutes
                                    break;
                                    }               
                            '\:' {  $timeFragments=[regex]::match($x,'(\d+)\:(\d+)')
                                    [Int16]$hours=$timeFragments.Groups[1].Value
                                    [Int16]$minutes=$timeFragments.Groups[2].Value
                                    $minutes + ($hours * 60)
                                    break
                                    }
                            default {$x}
                        }
                    }
                $properties.LogonTime = $sessionArray[5..($sessionArray.GetUpperBound(0))] -join ' '
                $result=New-Object -TypeName PSCustomObject -Property $properties
                return $result
                }
        
            foreach ($computer in $computername){
                try {
                    # Perusing legacy commandlets as there are no PowerShell equivalents at this time
                    $sessions=quser /server:$computer 2>&1 | Select-Object -Skip 1
                    ForEach ($line in $sessions) {               
                        $fragments = $line.Trim() -Replace '\s+',' ' -Split '\s'
                        $disconnectedSession=$fragments[2] -eq 'Disc' -or $fragments[3] -eq 'Disc'
                        if ($disconnectedSession){
                            $result=getDisconnectedSessionInfo $line $computer
                        }else{
                            $result=getActiveSessionInfo $line $computer
                        }
                        if($result){$results+=$result}
                    }
                }catch{
                    $results+=New-Object -TypeName PSCustomObject -Property @{
                        ComputerName=$computer
                        Error=$_.Exception.Message
                    } | Select-Object -Property UserName,ComputerName,SessionName,SessionId,State,IdleMinutes,LogonTime,Error|sort -Property UserName
                }
            }
            return $results
        }
        $activeUsers=(getSessionsInfo $computername|?{$_.State -eq 'Active'}).UserName
        return [PSCustomObject]@{
            computername=$computername
            os= $os
            cpuCount= $cpuCount
            cpuLoad= $cpuLoad
            ramCapacity= $memory
            ramUtilization= $memoryUtilization
            volumes=($volumes|out-string).trim()
            topProcesses=($topProcesses|out-string).trim()
            memoryUsagePerUser=($memoryUsagePerUser|out-string).trim()
            activeUsers=$activeUsers
        }
    }
    $results=@()
    foreach ($computer in $computernames){
        try{
            write-host "checking $computer..."
            $result=checkComputer $computer
            $results+=$result
        }catch{
            write-host $_            
        }
    }
    return $results
}

$systemUtilization=checkWindows $computernames
$systemUtilization|select-object computername,cpuCount,cpuLoad,ramCapacity,ramUtilization,activeUsers|ft -Wrap 
# computername  cpuCount cpuLoad ramCapacity ramUtilization activeUsers
# ------------  -------- ------- ----------- -------------- -----------
# LAX-RDSNODE01        8 1.00 %  64.00 GB    8.46 %         brucelee
# LAX-RDSNODE02        8 0.00 %  64.00 GB    21.90 %        leeSandwich
#                                                           mcDonalds
# LAX-RDSNODE03        8 48.00 % 64.00 GB    25.84 %        rambo
#                                                           mickeyMouse
# LAX-RDSNODE04        8 1.00 %  64.00 GB    21.90 %        oneMillionDollars
#                                                           20Cents