PowerShell / Win32-OpenSSH

Win32 port of OpenSSH
7.34k stars 755 forks source link

ssh.exe fails in PowerShell runspaces. #1301

Closed smru closed 4 years ago

smru commented 5 years ago

Dear fellow coders,

I have a PowerShell script to check what Windows computers are online and who is logged on. I have Bitvise SSH server installed on all Windows computers and I use ping.exe and plink.exe (from PuTTY 0.70). The script uses runspaces to send the ping.exe and plink.exe commands to all computers in parallel. The network has a maximum of 250 computers. The following line is used to get any user(s) logged on.

plink.exe -batch -i "$Env:UserProfile\id_rsa-auto.ppk" "Administrator@<IP Address>" "query.exe user"

Everything is working fine. However, when I replace plink.exe by ssh.exe on my Windows 10 1809 computer it works only for a small amount of computers. When there are too many computers to be queried I do get wrong results. I suspect there may be an issue with the re-entry code of ssh.exe.

ssh.exe -i "$Env:UserProfile\id_rsa-auto" -o "BatchMode=yes" "Administrator@<IP Address>" "$Command"

I'm not sure the back ticks will show up correctly online on Github. See attachment for exact PowerShell code.

Best regards,

Douwe Kiestra Status.txt

NoMoreFood commented 5 years ago

What version are you running? And what do you mean by "wrong results"?

smru commented 5 years ago

Hi Bryan,

I'm running Windows 10 Enterprise, Version 1809 (OS Build 17763.134). plink.exe:    Version 0.70.0.0, 2017-12-11 14:18, 617048 bytes. ssh.exe:      Version 7.7.2.1, 2018-09-15 16:07, 882688 bytes.               OpenSSH_for_Windows_7.7p1, LibreSSL 2.6.5

The PowerShell scripts and results are in the attachments.

I use the following two statements to easily toggle between using plink.exe or ssh.exe.

    $OpenSSH = $False     $OpenSSH = $True

The Remote-Good.ps1 script always succeeds. See Good.txt. The Remote-Fail.ps1 script always fails when probing more than say 10 computers. See Fail.txt. However, the Remote-Fail.ps1 script always succeeds when probing just 1 computer. See One.txt. The Remote-Fail.ps1 script sometimes succeeds and sometimes fails when probing 4 computers. See Four.txt.

Best regards, Douwe.

On 2018-12-05 10:47, Bryan Berns wrote:

What version are you running? And what do you mean by "wrong results"?

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/PowerShell/Win32-OpenSSH/issues/1301#issuecomment-444348707, or mute the thread https://github.com/notifications/unsubscribe-auth/ALGi4somGj59wXJp6aNTQ8AJWpC2-Ngeks5u10HYgaJpZM4Y9NyT.

################################################################################

REMOTE

################################################################################

See http://www.padisetty.com/2014/05/all-about-powershell-scriptblock.html

See help about_Scopes

function Add-Colors{ param([Parameter(Mandatory = $True)] $Status) $Field = $Status -split "\t+" if ($Field[0].Length -lt 0) { $Field[1] += "t" } if ($Field[1].Length -lt 8) { $Field[1] += "t" } if ($Field[2].Length -lt 8) { $Field[2] += "t" } if ($Field[3].Length -lt 8) { $Field[3] += "t" } if ($Field[3].Length -lt 16) { $Field[3] += "t" } if ($Field[4].Length -lt 8) { $Field[4] += "t" } if ($Field[5].Length -lt 0) { $Field[5] += "t" } if ($Field[6].Length -lt 8) { $Field[6] += "t" } if ($Field[6].Length -lt 16) { $Field[6] += "t" } if ($Field[6].Length -lt 24) { $Field[6] += "t" }

if ($Field[6].Length -lt 32) { $Field[6] += "`t" }

if ($Field[7].Length -lt  0) { $Field[7] += "`t" }
if ($Field.Count -gt 8) {
    if ($Field[8].Length -lt  8) { $Field[8] += "`t" }
    if ($Field[8].Length -lt 16) { $Field[8] += "`t" }

if ($Field[8].Length -lt 24) { $Field[8] += "`t" }

}
if ($Field.Count -gt 9) {
    if ($Field[9].Length -lt  8) { $Field[9] += "`t" }
    if ($Field[9].Length -lt 16) { $Field[9] += "`t" }
    if ($Field[9].Length -lt 24) { $Field[9] += "`t" }
    if ($Field[9].Length -lt 32) { $Field[9] += "`t" }
}
# Start writing status line with all fields.
if ($Field[7] -match "Bitvise") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Dns")     { Write-Host -ForegroundColor Gray    -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Key")     { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Locked")  { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Offline") { Write-Host -ForegroundColor Cyan    -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Online")  { Write-Host -ForegroundColor Green   -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Passwrd") { Write-Host -ForegroundColor Magenta -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Plink")   { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Refused") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Ssh")     { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Timeout") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Unknown") { Write-Host -ForegroundColor Gray    -NoNewLine "$($Field[0])`t" }
Write-Host -ForegroundColor Green -NoNewLine "$($Field[1])`t"
if ($Field[2]    -match "SMRU") { Write-Host -ForegroundColor Green  -NoNewLine "$($Field[2])`t" }
if ($Field[2] -notmatch "SMRU") { Write-Host -ForegroundColor Yellow -NoNewLine "$($Field[2])`t" }
Write-Host -ForegroundColor Green -NoNewLine "$($Field[3])`t"
Write-Host -ForegroundColor Green -NoNewLine "$($Field[4])`t"
if ($Field[5]    -match "32-bit") { Write-Host -ForegroundColor Yellow -NoNewLine "$($Field[5])`t" }
if ($Field[5] -notmatch "32-bit") { Write-Host -ForegroundColor Green  -NoNewLine "$($Field[5])`t" }
Write-Host -ForegroundColor Green -NoNewLine "$($Field[6])`t"
if ($Field[7] -match "Bitvise") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Dns")     { Write-Host -ForegroundColor Gray    -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Key")     { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Locked")  { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Offline") { Write-Host -ForegroundColor Cyan    -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Online")  { Write-Host -ForegroundColor Green   -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Passwrd") { Write-Host -ForegroundColor Magenta -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Plink")   { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Refused") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Ssh")     { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Timeout") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Unknown") { Write-Host -ForegroundColor Gray    -NoNewLine "$($Field[7])`t" }
if ($Field.Count -gt 8) {
    Write-Host -ForegroundColor Green -NoNewLine "$($Field[8])`t"
}
if ($Field.Count -gt 9) {
    Write-Host -ForegroundColor Green -NoNewLine "$($Field[9])"
}
Write-Host

}

Function: Get inventory information.

Parameters: The 1st parameter contains the inventory file.

Returns: The Chassis, Domain, Location, User, OSArch, OSName and OSVersion variables.

function Get-Inventory { param([Parameter(Mandatory = $True)] $File) $Chassis = "???" $Domain = "???" $Location = "???" $User = "???" $OSArch = "???" $OSName = "???" $OSVersion = "???" if (Test-Path -Path $File) { $Lines = Get-Content "$File" foreach ($Line in $Lines) { $Field = $Line -split "\t+" if ($Field) { if ($Field[0] -match "Chassis Type:") { $Chassis = $Field[1] } if ($Field[0] -match "Domain:") { $Domain = $Field[1] } if ($Field[0] -match "Location:") { $Location = $Field[1] } if ($Field[0] -match "User:") { $User = $Field[1] } if ($Field[0] -match "OS Architecture:") { $OSArch = $Field[1] } if ($Field[0] -match "OS Name:") { $OSName = $Field[1] } if ($Field[0] -match "OS Version:") { $OSVersion = $Field[1] } } } } else { Write-Host -ForegroundColor Magenta "File $File does not exist." } if ($OSVersion -ne "") { $OSName += " $OSVersion" }

Make sure not to have empty values.

if ($Chassis   -eq "") { $Chassis = " " }
if ($Domain    -eq "") { $Domain = " " }
if ($Location  -eq "") { $Location = " " }
if ($User      -eq "") { $User = " " }
if ($OSArch    -eq "") { $OSArch = " " }
if ($OSName    -eq "") { $OSName = " " }
if ($OSVersion -eq "") { $OSVersion = " " }
# Todo: Remove next 2 lines in next version.
if ($Domain -eq "smru.shoklo-unit.com") { $Domain = "SMRU" }
if ($Domain -eq "SMRU (smru.shoklo-unit.com)") { $Domain = "SMRU" }
if ($Domain.Length -lt 8) { $Domain += "`t" }
if ($Location.Length -lt 8) { $Location += "`t" }
if ($Location.Length -lt 16) { $Location += "`t" }
if ($User.Length -lt 8) { $User += "`t" }
$OSName = $OSName -replace " release", ""
if ($OsName.Length -lt 8) { $OSName += "`t" }
if ($OSName.Length -lt 16) { $OSName += "`t" }
if ($OSName.Length -lt 24) { $OSName += "`t" }
return @{
    Chassis = $Chassis
    Domain = $Domain
    Location = $Location
    User = $User
    OSArch = $OSArch
    OSName = $OSName
    OSVersion = $OSVersion
    # Create one or more consecutive tabs separated summary.
    Summary = "$Domain`t$Location`t$User`t$OSArch`t$OSName"
}

}

See https://learn-powershell.net/2012/05/10/speedy-network-information-query-using-powershell

See https://learn-powershell.net/2012/05/13/using-background-runspaces-instead-of-psjobs-for-better-performance

function Get-RunspaceData { $OpenSSH = $True $OpenSSH = $False foreach($Runspace in $Runspaces) {

Check if completed or timed out.

    if ($Runspace.Runspace.IsCompleted) {
        $Result = $Runspace.Powershell.EndInvoke($Runspace.Runspace)
        Add-Colors $Result
    } elseif ($Runspace.Runspace -ne $Null) {
        $Computer = $Runspace.Computer
        $Folder = $Runspace.Folder
        $Info = Get-Inventory "O:\Inventory\$Folder\$Computer.log"
        $Summary = $Info.Summary
        if ($OpenSSH) {
            $Result = "$Computer`t `t`t$Summary`tSsh"
        } else  {
            $Result = "$Computer`t `t`t$Summary`tPlink"
        }
        Add-Colors $Result
    }
    $Runspace.Powershell.Dispose()
    $Runspace.Runspace = $Null
    $Runspace.Powershell = $Null
}
# Clean out unused runspace jobs.
$temphash = $Runspaces.Clone()
$temphash | Where-Object { $_.Runspace -eq $Null } | ForEach-Object { $Runspaces.Remove($_) }

}

$ScriptBlock = { param($Computer, $Folder, $Info)

$OpenSSH = $True
$OpenSSH = $False

# Function: Send command to remote computer using plink.exe.
# Parameters:   The 1st parameter contains the command.
# Remarks:  Cannot use "<dos command> *> $Null" in PowerShell 2.0. Instead
#       use "cmd /c "<dos command> > nul 2>&1"". Any double quotes in
#       the <dos command> need to be escaped with the backtick character.
# Remarks:  Possible replies:
#       The server's host key does not match the one PuTTY has cached.
#       The server's host key is not cached in the registry.
# Remarks:  It is possible that plink.exe does not return due to some issue
#       on the remote computer.
#       See https://powershell.org/forums/topic/timeout-for-runspace
# Returns:  ???
function Send-PlinkCommand {
    param([Parameter(Mandatory = $True)] $Computer,
        [Parameter(Mandatory = $True)] $Command)
    $Command = "plink.exe -batch -i `"$Env:UserProfile\id_rsa-auto.ppk`" `"Administrator@$Computer`" `"$Command`" 2>&1"
    $Reply = Invoke-Expression $Command
    if ($Reply -match "The server's host key ") {
        # Update server's host key in PuTTY's cache if it does not match
        # or add server's host key to PuTTY's cache if it is not cached.
        # Putty's cache is HKCU:\Software\SimonTatham\PuTTY\SshHostKeys.
        # Use "(echo y)" to output a yes followed by Enter.
        $Cmd = "cmd /c `"(echo y) | $Command`""
        $Cmd = $Cmd -replace " -batch", ""
        $Null = Invoke-Expression $Cmd
        # Try again.
        $Reply = Invoke-Expression $Command
    }
    $Status = "Online"
    if ($Reply -match "FATAL ERROR: ") { $Status = "Bitvise" }
    if ($Reply -match "FATAL ERROR: Network error: Connection refused")   { $Status = "Refused" }
    if ($Reply -match "FATAL ERROR: Network error: Connection timed out") { $Status = "Timeout" }
    if ($Reply -match "FATAL ERROR: Server unexpectedly closed network connection") { $Status = "Locked" }
    if ($Reply -match "Server refused our key")  { $Status = "Passwrd" }
    if ($Reply -match "Unable to load key file") { $Status = "Key" }
    if ($Reply -match "Unable to use key file")  { $Status = "Key" }
    return @{
        Reply = $Reply
        Status = $Status
    }
}

# Function: Send command to remote computer using ssh.exe.
# Parameters:   The 1st parameter contains the command.
# Remarks:  Cannot use "<dos command> *> $Null" in PowerShell 2.0. Instead
#       use "cmd /c "<dos command> > nul 2>&1"". Any double quotes in
#       the <dos command> need to be escaped with the backtick character.
# Remarks:  Possible replies:
#       The server's host key does not match the one in known hosts.
#       The server's host key is not listed in known hosts.
# Remarks:  It is possible that ssh.exe does not return due to some issue
#       on the remote computer.
#       See https://powershell.org/forums/topic/timeout-for-runspace
# Returns:  ???
function Send-SshCommand {
    param([Parameter(Mandatory = $True)] $Computer,
        [Parameter(Mandatory = $True)] $Command)
    $Command = "ssh.exe -i `"$Env:UserProfile\id_rsa-auto`" -o `"BatchMode=yes`" `"Administrator@$Computer`" `"$Command`" 2>&1"
    $Reply = Invoke-Expression $Command

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

@ WARNING: UNPROTECTED PRIVATE KEY FILE! @

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

Permissions for 'C:\Users\Administrator\id_rsa-auto' are too open.

It is required that your private key files are NOT accessible by others.

This private key will be ignored.

Load key "C:\Users\Administrator\id_rsa-auto": bad permissions

Administrator@smrunb-ad10: Permission denied (publickey,password).

    if ($Reply -match "bad permissions") {
        msg.exe * "Permissions for 'C:\\Users\\Administrator\\id_rsa-auto' are too open."
    }
    if ($Reply -match "Host key verification failed") {
        # Update server's host key in list of known hosts if it does not match
        # or add server's host key to list of known hosts if it is not present.
        # List of OpenSSH known hosts is at C:\Users\<User>\.ssh\known_hosts.
        $Cmd = $Command -replace " -o `"BatchMode=yes`"", " -o `"BatchMode=yes`" -o `"StrictHostKeyChecking=no`""
        $Null = Invoke-Expression $Cmd
        # Try again.
        $Reply = Invoke-Expression $Command
    }
    $Status = "Online"

if ($Reply -match "FATAL ERROR: ") { $Status = "Bitvise" }

if ($Reply -match "FATAL ERROR: Network error: Connection refused") { $Status = "Refused" }

    if ($Reply -match "Connection timed out") { $Status = "Timeout" }
    if ($Reply -match "Connection closed by remote host") { $Status = "Locked" }
    if ($Reply -match "Load key") { $Status = "Key" }
    if ($Reply -match "No matching host key type found") { $Status = "Key" }
    if ($Reply -match "password") { $Status = "Passwrd" }
    return @{
        Reply = $Reply
        Status = $Status
    }
}

# Function: Get online status (Bitvise/Dns/Key/Locked/Offline/Online/
#       Passwrd/Plink/Refused/Ssh/Timeout/Unknown).
# Parameters:   The 1st parameter contains the computer name.
#       The 2nd parameter contains the inventory subfolder name.
#       The 3rd parameter contains the inventory information.
# Remarks:  When pinging a computer name it is possible that the DNS server
#       returns an outdated IP address. To ensure that the IP address
#       is correct, execute a remote command to tell its computer name
#       or check that the MAC address in the arp table matches the one
#       in the inventory.
# Remarks:  Information about the various statuses returned:
#       Bitvise Bitvise SSH Server fatal error.
#       Locked  Remote computer is locked out due to too many logon attempts.
#       Offline Ping of remote computer failed.
#       Dns Out of date DNS record.
#       Key Private key file issue.
#       Online  Public key authentication works.
#       Passwrd Public key authentication failed.
#       Plink   plink.exe command timed out.
#       Refused Connection refused.
#       Ssh ssh.exe command timed out.
#       Timeout Bitvise SSH Server is not installed or not started.
#       Unknown The computer name is not known by the DNS server.
# Returns:  The Status variable.
function Get-Status {
    param([Parameter(Mandatory = $True)] $Computer,
        [Parameter(Mandatory = $True)] $Folder,
        [Parameter(Mandatory = $True)] $Info)
    $Addr = " `t"
    $Count = 2
    $Summary = $Info.Summary
    $Ping = ping.exe -4 -n $Count -w 2500 $Computer
    if ($Ping -is [array]) {
        $Addr = $Ping[1] -replace '^.*?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*?$', '$1'
        $Unfixed = $Addr
        if ($Computer -eq $Env:ComputerName) {
            # Fix possible incorrect IP address due to VirtualBox.
            $Addr = nslookup.exe $Env:ComputerName | Where-Object { $_ -match "^Address:" } |
                Select-Object -Last 1
            $Addr = $Addr -replace "^Address:\s+", ""
        }
    }
    if ($Ping -match "Ping request could not find host")     { return "$Computer`t$Addr`t$Summary`tUnknown" }
    if ($Ping -match "Destination host unreachable")         { return "$Computer`t$Addr`t$Summary`tOffline" }
    if (($Ping -match "Request timed out").Count -eq $Count) { return "$Computer`t$Addr`t$Summary`tOffline" }
    if (($Ping -match "Reply from $Unfixed").Count -eq 0)    { return "$Computer`t$Addr`t$Summary`tOffline" }

    # Do not send commands to linux computers and windows servers.
    if ($Folder -eq "_Servers") {
        if ($Info.Chassis -match "Virtual") { return "$Computer`t$Addr`t$Summary`tOnline`t-" }
    }
    if ($Info.OSName -match "Debian") { return "$Computer`t$Addr`t$Summary`tOnline`t-" }
    if ($Info.OSName -match "Endian") { return "$Computer`t$Addr`t$Summary`tOnline`t-" }
    if ($Info.OSName -match "Server") { return "$Computer`t$Addr`t$Summary`tOnline`t-" }

    $Users = " "

    # Get computer name from remote computer.
    $Command = "set ComputerName"
    if ($OpenSSH) {
        $Answer = Send-SshCommand $Computer $Command
    } else {
        $Answer = Send-PlinkCommand $Computer $Command
    }
    if ($Answer.Status -ne "Online") { return "$Computer`t$Addr`t$Summary`t$($Answer.Status)`t$Users`t$($Answer.Reply)" }
    if ($Answer.Reply) {
        $RemoteName = $Answer.Reply.Split('=')[1]
        # Check if out of date DNS entry.
        if ($Computer -ne $RemoteName) { return "$Computer`t `t`t$Summary`tDns`t$Users`t$($Answer.Reply)" }
    }

    # Get logged on user names on remote computer.
    $Command = "query.exe user"
    if ($OpenSSH) {
        $Answer = Send-SshCommand $Computer $Command
    } else {
        $Answer = Send-PlinkCommand $Computer $Command
    }
    if ($Answer.Status -ne "Online") { return "$Computer`t$Addr`t$Summary`t$($Answer.Status)`t$Users`t$($Answer.Reply)" }
    # Maximum length of user name is 20 characters.
    $Users = ((($Answer.Reply | Select-Object -Skip 1) -replace '^.(.{1,20}).*$', '$1').Trim() | Sort-Object) -join ", "

    # Get Windows version on remote computer.
    $Version = ""
    $Command = "ver"
    if ($OpenSSH) {
        $Answer = Send-SshCommand $Computer $Command
    } else {
        $Answer = Send-PlinkCommand $Computer $Command
    }
    if ($Answer.Status -ne "Online") { return "$Computer`t$Addr`t$Summary`t$($Answer.Status)`t$Users`t$($Answer.Reply)" }
    if ($Answer.Reply -match "10240") { $Version = "1507" }
    if ($Answer.Reply -match "10586") { $Version = "1511" }
    if ($Answer.Reply -match "14393") { $Version = "1607" }
    if ($Answer.Reply -match "15063") { $Version = "1703" }
    if ($Answer.Reply -match "16299") { $Version = "1709" }
    if ($Answer.Reply -match "17134") { $Version = "1803" }
    if ($Answer.Reply -match "17763") { $Version = "1809" }
    # Find Windows 10 computers where inventory and current version do not match.
    if ($Summary -notmatch $Version) {
        $Summary = $Summary -replace "^(.*)($($Info.OSVersion))(.*)$", "`$1`$2/$Version`$3"
    }

    return "$Computer`t$Addr`t$Summary`tOnline`t$Users`t$($Answer.Reply)"
}

Get-Status $Computer $Folder $Info

}

Function: Show online status of SMRU computers.

Parameters: The 1st optional parameter contains the site name.

Returns: None.

function Show-Status { param([Parameter(Mandatory = $False)] $Site = "MST") $OpenSSH = $True $OpenSSH = $False if ($OpenSSH) {

Clear list of OpenSSH known hosts.

    $Null = Remove-Item -ErrorAction SilentlyContinue -Force -Path "$Env:UserProfile\.ssh\known_hosts"
}
$Throttle = 15
$Path = "O:\Inventory"
$File = "$Path\Computers3.txt"

if (Test-Path -Path $File) {
    $Header =
        "Computer"      + "`t" +
        "IP Address"    + "`t" +
        "Domain"        + "`t`t" +
        "Location"      + "`t`t" +
        "User"      + "`t`t" +
        "OS Arch"       + "`t" +
        "OS Name"       + "`t`t`t`t" +
        "Status"        + "`t" +
        "Logged on users"   + "`t`t" +
        "Reply"     + "`t`t`t`t"
    $Separator =
        "-" * 15 + "`t" +
        "-" * 15 + "`t" +
        "-" * 15 + "`t" +
        "-" * 23 + "`t" +
        "-" * 15 + "`t" +
        "-" *  7 + "`t" +
        "-" * 31 + "`t" +
        "-" *  7 + "`t" +
        "-" * 23 + "`t" +
        "-" * 31

    Write-Host -Foregroundcolor Green $Header
    Write-Host -Foregroundcolor Green $Separator

    # Create runspace pool and session states.
    $SessionState = [system.management.automation.runspaces.initialsessionState]::CreateDefault()
    $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $Throttle, $SessionState, $Host)
    $RunspacePool.Open()

    # Create empty collection to hold runspace jobs.
    $Script:Runspaces = New-Object System.Collections.ArrayList

    # Total number of run spaces.
    $Total = 0

    # One or more consecutive tabs separated file.
    # Field 0:  Hostname
    # Field 1:  Folder
    # Field 2:  Site
    # Field 3:  Address
    # Field 4:  Location
    # Field 5:  Remark
    $Lines = Get-Content "$File"
    foreach ($Line in $Lines) {
        $Field = $Line -split "\t+"
        # Do not show servers at MST.
        if (($Field) -and
            ($Field[0] -match "SMRU") -and
            ($Field[2] -eq $Site) -and
            (($Field[2] -ne "MST") -and ($Field[1] -eq "_Servers") -or ($Field[1] -notmatch "_")) -and
            (($Field[2] -ne "MST") -or ($Field[3] -eq " ")))
        {
            $Total++
            $Info = Get-Inventory "$Path\$($Field[1])\$($Field[0]).log"
            # Create powershell instance and supply scriptblock with other parameters.
            $PowerShell = [powershell]::Create().AddScript($ScriptBlock).AddArgument($Field[0]).AddArgument($Field[1]).AddArgument($Info)

            # Add runspace into powershell instance.
            $PowerShell.RunspacePool = $RunspacePool

            # Create temporary collection for each runspace.
            $Temp = "" | Select-Object Computer, Folder, PowerShell, Runspace
            $Temp.Computer = $Field[0]
            $Temp.Folder = $Field[1]
            $Temp.PowerShell = $PowerShell

            # Save handle output when calling BeginInvoke() that will be used later to end runspace.
            $Temp.Runspace = $PowerShell.BeginInvoke()
            $Null = $Runspaces.Add($Temp)
        }
    }

    if ($Runspaces) {
        # Reset progress indicator.
        $BlankLine = " " * ([Console]::BufferWidth - 1)

        # Wait for all runspaces to have completed.
        $Loops = 0
        while (($Runspaces.Runspace.IsCompleted -contains $False) -and ($Loops -lt 35)) {
            $Loops++
            # Use @() to make sure it always returns an array that has the Count property.
            # See answer to question 2 at https://www.red-gate.com/simple-talk/sysadmin/powershell/a-plethora-of-powershell-pitfalls
            $Tasks = @($Runspaces | Where-Object { $_.Runspace.IsCompleted -eq $False })
            $Completed = $Total - $Tasks.Count
            # See https://stackoverflow.com/questions/4998173/how-do-i-write-to-standard-error-in-powershell
            # Show progress line using standard error output.
            if ($Tasks.Count -lt 9) {
                # Get list of computers that have not yet finished.
                $Computers = ""
                foreach ($Task in $Tasks) { $Computers += ", $($Task.Computer)" }
                $Computers = $Computers -replace "^, ", ""
                [Console]::Error.Write("$BlankLine`r$Completed out of $Total tasks completed. Still waiting for: $Computers`r")
            } else {
                [Console]::Error.Write("$BlankLine`r$Completed out of $Total tasks completed.`r")
            }
            Start-Sleep -Seconds 1
        }

        # Clear progress line.
        [Console]::Error.Write("$BlankLine`r")

        # Finish processing the runspace jobs.
        Get-RunspaceData

        # Close runspace pool.
        $RunspacePool.Close()
    }
} else {
    Write-Host -ForegroundColor Magenta "File '$File' is not available, aborting..."
}

}

Main code that is appended to all the .ps1 powershell files. It allows

running a .ps1 powershell file with an argument that is an expression

or a function (including any arguments) defined in that .ps1 file.

Invoke expression or function with any given arguments.

$Command = "" if ($Args) { for ($i = 0; $i -lt $Args.Count; $i++) { $Command += "$($Args[$i]) " } Invoke-Expression -Command $Command }

################################################################################

REMOTE

################################################################################

See http://www.padisetty.com/2014/05/all-about-powershell-scriptblock.html

See help about_Scopes

function Add-Colors{ param([Parameter(Mandatory = $True)] $Status) $Field = $Status -split "\t+" if ($Field[0].Length -lt 0) { $Field[1] += "t" } if ($Field[1].Length -lt 8) { $Field[1] += "t" } if ($Field[2].Length -lt 8) { $Field[2] += "t" } if ($Field[3].Length -lt 8) { $Field[3] += "t" } if ($Field[3].Length -lt 16) { $Field[3] += "t" } if ($Field[4].Length -lt 8) { $Field[4] += "t" } if ($Field[5].Length -lt 0) { $Field[5] += "t" } if ($Field[6].Length -lt 8) { $Field[6] += "t" } if ($Field[6].Length -lt 16) { $Field[6] += "t" } if ($Field[6].Length -lt 24) { $Field[6] += "t" }

if ($Field[6].Length -lt 32) { $Field[6] += "`t" }

if ($Field[7].Length -lt  0) { $Field[7] += "`t" }
if ($Field.Count -gt 8) {
    if ($Field[8].Length -lt  8) { $Field[8] += "`t" }
    if ($Field[8].Length -lt 16) { $Field[8] += "`t" }

if ($Field[8].Length -lt 24) { $Field[8] += "`t" }

}
if ($Field.Count -gt 9) {
    if ($Field[9].Length -lt  8) { $Field[9] += "`t" }
    if ($Field[9].Length -lt 16) { $Field[9] += "`t" }
    if ($Field[9].Length -lt 24) { $Field[9] += "`t" }
    if ($Field[9].Length -lt 32) { $Field[9] += "`t" }
}
# Start writing status line with all fields.
if ($Field[7] -match "Bitvise") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Dns")     { Write-Host -ForegroundColor Gray    -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Key")     { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Locked")  { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Offline") { Write-Host -ForegroundColor Cyan    -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Online")  { Write-Host -ForegroundColor Green   -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Passwrd") { Write-Host -ForegroundColor Magenta -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Plink")   { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Refused") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Ssh")     { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Timeout") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[0])`t" }
if ($Field[7] -match "Unknown") { Write-Host -ForegroundColor Gray    -NoNewLine "$($Field[0])`t" }
Write-Host -ForegroundColor Green -NoNewLine "$($Field[1])`t"
if ($Field[2]    -match "SMRU") { Write-Host -ForegroundColor Green  -NoNewLine "$($Field[2])`t" }
if ($Field[2] -notmatch "SMRU") { Write-Host -ForegroundColor Yellow -NoNewLine "$($Field[2])`t" }
Write-Host -ForegroundColor Green -NoNewLine "$($Field[3])`t"
Write-Host -ForegroundColor Green -NoNewLine "$($Field[4])`t"
if ($Field[5]    -match "32-bit") { Write-Host -ForegroundColor Yellow -NoNewLine "$($Field[5])`t" }
if ($Field[5] -notmatch "32-bit") { Write-Host -ForegroundColor Green  -NoNewLine "$($Field[5])`t" }
Write-Host -ForegroundColor Green -NoNewLine "$($Field[6])`t"
if ($Field[7] -match "Bitvise") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Dns")     { Write-Host -ForegroundColor Gray    -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Key")     { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Locked")  { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Offline") { Write-Host -ForegroundColor Cyan    -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Online")  { Write-Host -ForegroundColor Green   -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Passwrd") { Write-Host -ForegroundColor Magenta -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Plink")   { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Refused") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Ssh")     { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Timeout") { Write-Host -ForegroundColor Red     -NoNewLine "$($Field[7])`t" }
if ($Field[7] -match "Unknown") { Write-Host -ForegroundColor Gray    -NoNewLine "$($Field[7])`t" }
if ($Field.Count -gt 8) {
    Write-Host -ForegroundColor Green -NoNewLine "$($Field[8])`t"
}
if ($Field.Count -gt 9) {
    Write-Host -ForegroundColor Green -NoNewLine "$($Field[9])"
}
Write-Host

}

Function: Get inventory information.

Parameters: The 1st parameter contains the inventory file.

Returns: The Chassis, Domain, Location, User, OSArch, OSName and OSVersion variables.

function Get-Inventory { param([Parameter(Mandatory = $True)] $File) $Chassis = "???" $Domain = "???" $Location = "???" $User = "???" $OSArch = "???" $OSName = "???" $OSVersion = "???" if (Test-Path -Path $File) { $Lines = Get-Content "$File" foreach ($Line in $Lines) { $Field = $Line -split "\t+" if ($Field) { if ($Field[0] -match "Chassis Type:") { $Chassis = $Field[1] } if ($Field[0] -match "Domain:") { $Domain = $Field[1] } if ($Field[0] -match "Location:") { $Location = $Field[1] } if ($Field[0] -match "User:") { $User = $Field[1] } if ($Field[0] -match "OS Architecture:") { $OSArch = $Field[1] } if ($Field[0] -match "OS Name:") { $OSName = $Field[1] } if ($Field[0] -match "OS Version:") { $OSVersion = $Field[1] } } } } else { Write-Host -ForegroundColor Magenta "File $File does not exist." } if ($OSVersion -ne "") { $OSName += " $OSVersion" }

Make sure not to have empty values.

if ($Chassis   -eq "") { $Chassis = " " }
if ($Domain    -eq "") { $Domain = " " }
if ($Location  -eq "") { $Location = " " }
if ($User      -eq "") { $User = " " }
if ($OSArch    -eq "") { $OSArch = " " }
if ($OSName    -eq "") { $OSName = " " }
if ($OSVersion -eq "") { $OSVersion = " " }
# Todo: Remove next 2 lines in next version.
if ($Domain -eq "smru.shoklo-unit.com") { $Domain = "SMRU" }
if ($Domain -eq "SMRU (smru.shoklo-unit.com)") { $Domain = "SMRU" }
if ($Domain.Length -lt 8) { $Domain += "`t" }
if ($Location.Length -lt 8) { $Location += "`t" }
if ($Location.Length -lt 16) { $Location += "`t" }
if ($User.Length -lt 8) { $User += "`t" }
$OSName = $OSName -replace " release", ""
if ($OsName.Length -lt 8) { $OSName += "`t" }
if ($OSName.Length -lt 16) { $OSName += "`t" }
if ($OSName.Length -lt 24) { $OSName += "`t" }
return @{
    Chassis = $Chassis
    Domain = $Domain
    Location = $Location
    User = $User
    OSArch = $OSArch
    OSName = $OSName
    OSVersion = $OSVersion
    # Create one or more consecutive tabs separated summary.
    Summary = "$Domain`t$Location`t$User`t$OSArch`t$OSName"
}

}

See https://learn-powershell.net/2012/05/10/speedy-network-information-query-using-powershell

See https://learn-powershell.net/2012/05/13/using-background-runspaces-instead-of-psjobs-for-better-performance

function Get-RunspaceData { $OpenSSH = $False $OpenSSH = $True foreach($Runspace in $Runspaces) {

Check if completed or timed out.

    if ($Runspace.Runspace.IsCompleted) {
        $Result = $Runspace.Powershell.EndInvoke($Runspace.Runspace)
        Add-Colors $Result
    } elseif ($Runspace.Runspace -ne $Null) {
        $Computer = $Runspace.Computer
        $Folder = $Runspace.Folder
        $Info = Get-Inventory "O:\Inventory\$Folder\$Computer.log"
        $Summary = $Info.Summary
        if ($OpenSSH) {
            $Result = "$Computer`t `t`t$Summary`tSsh"
        } else  {
            $Result = "$Computer`t `t`t$Summary`tPlink"
        }
        Add-Colors $Result
    }
    $Runspace.Powershell.Dispose()
    $Runspace.Runspace = $Null
    $Runspace.Powershell = $Null
}
# Clean out unused runspace jobs.
$temphash = $Runspaces.Clone()
$temphash | Where-Object { $_.Runspace -eq $Null } | ForEach-Object { $Runspaces.Remove($_) }

}

$ScriptBlock = { param($Computer, $Folder, $Info)

$OpenSSH = $False
$OpenSSH = $True

# Function: Send command to remote computer using plink.exe.
# Parameters:   The 1st parameter contains the command.
# Remarks:  Cannot use "<dos command> *> $Null" in PowerShell 2.0. Instead
#       use "cmd /c "<dos command> > nul 2>&1"". Any double quotes in
#       the <dos command> need to be escaped with the backtick character.
# Remarks:  Possible replies:
#       The server's host key does not match the one PuTTY has cached.
#       The server's host key is not cached in the registry.
# Remarks:  It is possible that plink.exe does not return due to some issue
#       on the remote computer.
#       See https://powershell.org/forums/topic/timeout-for-runspace
# Returns:  ???
function Send-PlinkCommand {
    param([Parameter(Mandatory = $True)] $Computer,
        [Parameter(Mandatory = $True)] $Command)
    $Command = "plink.exe -batch -i `"$Env:UserProfile\id_rsa-auto.ppk`" `"Administrator@$Computer`" `"$Command`" 2>&1"
    $Reply = Invoke-Expression $Command
    if ($Reply -match "The server's host key ") {
        # Update server's host key in PuTTY's cache if it does not match
        # or add server's host key to PuTTY's cache if it is not cached.
        # Putty's cache is HKCU:\Software\SimonTatham\PuTTY\SshHostKeys.
        # Use "(echo y)" to output a yes followed by Enter.
        $Cmd = "cmd /c `"(echo y) | $Command`""
        $Cmd = $Cmd -replace " -batch", ""
        $Null = Invoke-Expression $Cmd
        # Try again.
        $Reply = Invoke-Expression $Command
    }
    $Status = "Online"
    if ($Reply -match "FATAL ERROR: ") { $Status = "Bitvise" }
    if ($Reply -match "FATAL ERROR: Network error: Connection refused")   { $Status = "Refused" }
    if ($Reply -match "FATAL ERROR: Network error: Connection timed out") { $Status = "Timeout" }
    if ($Reply -match "FATAL ERROR: Server unexpectedly closed network connection") { $Status = "Locked" }
    if ($Reply -match "Server refused our key")  { $Status = "Passwrd" }
    if ($Reply -match "Unable to load key file") { $Status = "Key" }
    if ($Reply -match "Unable to use key file")  { $Status = "Key" }
    return @{
        Reply = $Reply
        Status = $Status
    }
}

# Function: Send command to remote computer using ssh.exe.
# Parameters:   The 1st parameter contains the command.
# Remarks:  Cannot use "<dos command> *> $Null" in PowerShell 2.0. Instead
#       use "cmd /c "<dos command> > nul 2>&1"". Any double quotes in
#       the <dos command> need to be escaped with the backtick character.
# Remarks:  Possible replies:
#       The server's host key does not match the one in known hosts.
#       The server's host key is not listed in known hosts.
# Remarks:  It is possible that ssh.exe does not return due to some issue
#       on the remote computer.
#       See https://powershell.org/forums/topic/timeout-for-runspace
# Returns:  ???
function Send-SshCommand {
    param([Parameter(Mandatory = $True)] $Computer,
        [Parameter(Mandatory = $True)] $Command)
    $Command = "ssh.exe -i `"$Env:UserProfile\id_rsa-auto`" -o `"BatchMode=yes`" `"Administrator@$Computer`" `"$Command`" 2>&1"
    $Reply = Invoke-Expression $Command

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

@ WARNING: UNPROTECTED PRIVATE KEY FILE! @

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

Permissions for 'C:\Users\Administrator\id_rsa-auto' are too open.

It is required that your private key files are NOT accessible by others.

This private key will be ignored.

Load key "C:\Users\Administrator\id_rsa-auto": bad permissions

Administrator@smrunb-ad10: Permission denied (publickey,password).

    if ($Reply -match "bad permissions") {
        msg.exe * "Permissions for 'C:\\Users\\Administrator\\id_rsa-auto' are too open."
    }
    if ($Reply -match "Host key verification failed") {
        # Update server's host key in list of known hosts if it does not match
        # or add server's host key to list of known hosts if it is not present.
        # List of OpenSSH known hosts is at C:\Users\<User>\.ssh\known_hosts.
        $Cmd = $Command -replace " -o `"BatchMode=yes`"", " -o `"BatchMode=yes`" -o `"StrictHostKeyChecking=no`""
        $Null = Invoke-Expression $Cmd
        # Try again.
        $Reply = Invoke-Expression $Command
    }
    $Status = "Online"

if ($Reply -match "FATAL ERROR: ") { $Status = "Bitvise" }

if ($Reply -match "FATAL ERROR: Network error: Connection refused") { $Status = "Refused" }

    if ($Reply -match "Connection timed out") { $Status = "Timeout" }
    if ($Reply -match "Connection closed by remote host") { $Status = "Locked" }
    if ($Reply -match "Load key") { $Status = "Key" }
    if ($Reply -match "No matching host key type found") { $Status = "Key" }
    if ($Reply -match "password") { $Status = "Passwrd" }
    return @{
        Reply = $Reply
        Status = $Status
    }
}

# Function: Get online status (Bitvise/Dns/Key/Locked/Offline/Online/
#       Passwrd/Plink/Refused/Ssh/Timeout/Unknown).
# Parameters:   The 1st parameter contains the computer name.
#       The 2nd parameter contains the inventory subfolder name.
#       The 3rd parameter contains the inventory information.
# Remarks:  When pinging a computer name it is possible that the DNS server
#       returns an outdated IP address. To ensure that the IP address
#       is correct, execute a remote command to tell its computer name
#       or check that the MAC address in the arp table matches the one
#       in the inventory.
# Remarks:  Information about the various statuses returned:
#       Bitvise Bitvise SSH Server fatal error.
#       Locked  Remote computer is locked out due to too many logon attempts.
#       Offline Ping of remote computer failed.
#       Dns Out of date DNS record.
#       Key Private key file issue.
#       Online  Public key authentication works.
#       Passwrd Public key authentication failed.
#       Plink   plink.exe command timed out.
#       Refused Connection refused.
#       Ssh ssh.exe command timed out.
#       Timeout Bitvise SSH Server is not installed or not started.
#       Unknown The computer name is not known by the DNS server.
# Returns:  The Status variable.
function Get-Status {
    param([Parameter(Mandatory = $True)] $Computer,
        [Parameter(Mandatory = $True)] $Folder,
        [Parameter(Mandatory = $True)] $Info)
    $Addr = " `t"
    $Count = 2
    $Summary = $Info.Summary
    $Ping = ping.exe -4 -n $Count -w 2500 $Computer
    if ($Ping -is [array]) {
        $Addr = $Ping[1] -replace '^.*?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*?$', '$1'
        $Unfixed = $Addr
        if ($Computer -eq $Env:ComputerName) {
            # Fix possible incorrect IP address due to VirtualBox.
            $Addr = nslookup.exe $Env:ComputerName | Where-Object { $_ -match "^Address:" } |
                Select-Object -Last 1
            $Addr = $Addr -replace "^Address:\s+", ""
        }
    }
    if ($Ping -match "Ping request could not find host")     { return "$Computer`t$Addr`t$Summary`tUnknown" }
    if ($Ping -match "Destination host unreachable")         { return "$Computer`t$Addr`t$Summary`tOffline" }
    if (($Ping -match "Request timed out").Count -eq $Count) { return "$Computer`t$Addr`t$Summary`tOffline" }
    if (($Ping -match "Reply from $Unfixed").Count -eq 0)    { return "$Computer`t$Addr`t$Summary`tOffline" }

    # Do not send commands to linux computers and windows servers.
    if ($Folder -eq "_Servers") {
        if ($Info.Chassis -match "Virtual") { return "$Computer`t$Addr`t$Summary`tOnline`t-" }
    }
    if ($Info.OSName -match "Debian") { return "$Computer`t$Addr`t$Summary`tOnline`t-" }
    if ($Info.OSName -match "Endian") { return "$Computer`t$Addr`t$Summary`tOnline`t-" }
    if ($Info.OSName -match "Server") { return "$Computer`t$Addr`t$Summary`tOnline`t-" }

    $Users = " "

    # Get computer name from remote computer.
    $Command = "set ComputerName"
    if ($OpenSSH) {
        $Answer = Send-SshCommand $Computer $Command
    } else {
        $Answer = Send-PlinkCommand $Computer $Command
    }
    if ($Answer.Status -ne "Online") { return "$Computer`t$Addr`t$Summary`t$($Answer.Status)`t$Users`t$($Answer.Reply)" }
    if ($Answer.Reply) {
        $RemoteName = $Answer.Reply.Split('=')[1]
        # Check if out of date DNS entry.
        if ($Computer -ne $RemoteName) { return "$Computer`t `t`t$Summary`tDns`t$Users`t$($Answer.Reply)" }
    }

    # Get logged on user names on remote computer.
    $Command = "query.exe user"
    if ($OpenSSH) {
        $Answer = Send-SshCommand $Computer $Command
    } else {
        $Answer = Send-PlinkCommand $Computer $Command
    }
    if ($Answer.Status -ne "Online") { return "$Computer`t$Addr`t$Summary`t$($Answer.Status)`t$Users`t$($Answer.Reply)" }
    # Maximum length of user name is 20 characters.
    $Users = ((($Answer.Reply | Select-Object -Skip 1) -replace '^.(.{1,20}).*$', '$1').Trim() | Sort-Object) -join ", "

    # Get Windows version on remote computer.
    $Version = ""
    $Command = "ver"
    if ($OpenSSH) {
        $Answer = Send-SshCommand $Computer $Command
    } else {
        $Answer = Send-PlinkCommand $Computer $Command
    }
    if ($Answer.Status -ne "Online") { return "$Computer`t$Addr`t$Summary`t$($Answer.Status)`t$Users`t$($Answer.Reply)" }
    if ($Answer.Reply -match "10240") { $Version = "1507" }
    if ($Answer.Reply -match "10586") { $Version = "1511" }
    if ($Answer.Reply -match "14393") { $Version = "1607" }
    if ($Answer.Reply -match "15063") { $Version = "1703" }
    if ($Answer.Reply -match "16299") { $Version = "1709" }
    if ($Answer.Reply -match "17134") { $Version = "1803" }
    if ($Answer.Reply -match "17763") { $Version = "1809" }
    # Find Windows 10 computers where inventory and current version do not match.
    if ($Summary -notmatch $Version) {
        $Summary = $Summary -replace "^(.*)($($Info.OSVersion))(.*)$", "`$1`$2/$Version`$3"
    }

    return "$Computer`t$Addr`t$Summary`tOnline`t$Users`t$($Answer.Reply)"
}

Get-Status $Computer $Folder $Info

}

Function: Show online status of SMRU computers.

Parameters: The 1st optional parameter contains the site name.

Returns: None.

function Show-Status { param([Parameter(Mandatory = $False)] $Site = "MST") $OpenSSH = $False $OpenSSH = $True if ($OpenSSH) {

Clear list of OpenSSH known hosts.

    $Null = Remove-Item -ErrorAction SilentlyContinue -Force -Path "$Env:UserProfile\.ssh\known_hosts"
}
$Throttle = 15
$Path = "O:\Inventory"
$File = "$Path\Computers3.txt"

if (Test-Path -Path $File) {
    $Header =
        "Computer"      + "`t" +
        "IP Address"    + "`t" +
        "Domain"        + "`t`t" +
        "Location"      + "`t`t" +
        "User"      + "`t`t" +
        "OS Arch"       + "`t" +
        "OS Name"       + "`t`t`t`t" +
        "Status"        + "`t" +
        "Logged on users"   + "`t`t" +
        "Reply"     + "`t`t`t`t"
    $Separator =
        "-" * 15 + "`t" +
        "-" * 15 + "`t" +
        "-" * 15 + "`t" +
        "-" * 23 + "`t" +
        "-" * 15 + "`t" +
        "-" *  7 + "`t" +
        "-" * 31 + "`t" +
        "-" *  7 + "`t" +
        "-" * 23 + "`t" +
        "-" * 31

    Write-Host -Foregroundcolor Green $Header
    Write-Host -Foregroundcolor Green $Separator

    # Create runspace pool and session states.
    $SessionState = [system.management.automation.runspaces.initialsessionState]::CreateDefault()
    $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $Throttle, $SessionState, $Host)
    $RunspacePool.Open()

    # Create empty collection to hold runspace jobs.
    $Script:Runspaces = New-Object System.Collections.ArrayList

    # Total number of run spaces.
    $Total = 0

    # One or more consecutive tabs separated file.
    # Field 0:  Hostname
    # Field 1:  Folder
    # Field 2:  Site
    # Field 3:  Address
    # Field 4:  Location
    # Field 5:  Remark
    $Lines = Get-Content "$File"
    foreach ($Line in $Lines) {
        $Field = $Line -split "\t+"
        # Do not show servers at MST.
        if (($Field) -and
            ($Field[0] -match "SMRU") -and
            ($Field[2] -eq $Site) -and
            (($Field[2] -ne "MST") -and ($Field[1] -eq "_Servers") -or ($Field[1] -notmatch "_")) -and
            (($Field[2] -ne "MST") -or ($Field[3] -eq " ")))
        {
            $Total++
            $Info = Get-Inventory "$Path\$($Field[1])\$($Field[0]).log"
            # Create powershell instance and supply scriptblock with other parameters.
            $PowerShell = [powershell]::Create().AddScript($ScriptBlock).AddArgument($Field[0]).AddArgument($Field[1]).AddArgument($Info)

            # Add runspace into powershell instance.
            $PowerShell.RunspacePool = $RunspacePool

            # Create temporary collection for each runspace.
            $Temp = "" | Select-Object Computer, Folder, PowerShell, Runspace
            $Temp.Computer = $Field[0]
            $Temp.Folder = $Field[1]
            $Temp.PowerShell = $PowerShell

            # Save handle output when calling BeginInvoke() that will be used later to end runspace.
            $Temp.Runspace = $PowerShell.BeginInvoke()
            $Null = $Runspaces.Add($Temp)
        }
    }

    if ($Runspaces) {
        # Reset progress indicator.
        $BlankLine = " " * ([Console]::BufferWidth - 1)

        # Wait for all runspaces to have completed.
        $Loops = 0
        while (($Runspaces.Runspace.IsCompleted -contains $False) -and ($Loops -lt 35)) {
            $Loops++
            # Use @() to make sure it always returns an array that has the Count property.
            # See answer to question 2 at https://www.red-gate.com/simple-talk/sysadmin/powershell/a-plethora-of-powershell-pitfalls
            $Tasks = @($Runspaces | Where-Object { $_.Runspace.IsCompleted -eq $False })
            $Completed = $Total - $Tasks.Count
            # See https://stackoverflow.com/questions/4998173/how-do-i-write-to-standard-error-in-powershell
            # Show progress line using standard error output.
            if ($Tasks.Count -lt 9) {
                # Get list of computers that have not yet finished.
                $Computers = ""
                foreach ($Task in $Tasks) { $Computers += ", $($Task.Computer)" }
                $Computers = $Computers -replace "^, ", ""
                [Console]::Error.Write("$BlankLine`r$Completed out of $Total tasks completed. Still waiting for: $Computers`r")
            } else {
                [Console]::Error.Write("$BlankLine`r$Completed out of $Total tasks completed.`r")
            }
            Start-Sleep -Seconds 1
        }

        # Clear progress line.
        [Console]::Error.Write("$BlankLine`r")

        # Finish processing the runspace jobs.
        Get-RunspaceData

        # Close runspace pool.
        $RunspacePool.Close()
    }
} else {
    Write-Host -ForegroundColor Magenta "File '$File' is not available, aborting..."
}

}

Main code that is appended to all the .ps1 powershell files. It allows

running a .ps1 powershell file with an argument that is an expression

or a function (including any arguments) defined in that .ps1 file.

Invoke expression or function with any given arguments.

$Command = "" if ($Args) { for ($i = 0; $i -lt $Args.Count; $i++) { $Command += "$($Args[$i]) " } Invoke-Expression -Command $Command }

SMRUWS-IT10\Administrator C:\Users\Administrator# powershell -ExecutionPolicy Bypass -File "C:\Tmp\Remote-Good.ps1" Show-Status Computer IP Address Domain Location User OS Arch OS Name Status Logged on users Reply


SMRUWS-AD01 10.10.1.79 SMRU ANC office Mookhopaw 64-bit Windows 10 Enterprise 1809 Offline SMRUWS-AD04 10.10.1.80 SMRU Data office Miasa 64-bit Windows 7 Professional Online miasa Microsoft Windows [Version 6.1.7601] SMRUWS-AD05 10.10.1.75 SMRU Finance office Amorn 64-bit Windows 7 Professional Offline SMRUWS-AD06 10.10.1.109 SMRU Finance office Kee 64-bit Windows 7 Professional Offline SMRUWS-AD07 10.10.1.122 SMRU Admin office Sittipong 64-bit Windows 7 Professional Offline SMRUWS-AD11 SMRU METF office 2 Micho 64-bit Windows 10 Enterprise 1809 Unknown SMRUWS-AD12 10.10.1.78 SMRU Pharmacy room Carela 64-bit Windows 7 Professional Online carela Microsoft Windows [Version 6.1.7601] SMRUWS-AD14 SMRU Finance office Jeerawan.w 64-bit Windows 7 Professional Unknown SMRUWS-AD15 10.10.1.84 SMRU Logistics Siam 64-bit Windows 7 Professional Online siam Microsoft Windows [Version 6.1.7601] SMRUWS-AD17 10.10.1.70 SMRU Finance office Janjira 64-bit Windows 7 Professional Online janjira Microsoft Windows [Version 6.1.7601] SMRUWS-AD18 10.10.1.147 SMRU Staff office Sangrawee 64-bit Windows 7 Professional Online Microsoft Windows [Version 6.1.7601] SMRUWS-AD19 10.10.1.136 SMRU HR Suchadas 64-bit Windows 8.1 Enterprise Online suchadas Microsoft Windows [Version 6.3.9600] SMRUWS-AD20 10.10.1.181 SMRU Grant Phaitoon 64-bit Windows 8.1 Enterprise Online phaitoon Microsoft Windows [Version 6.3.9600] SMRUWS-AD21 10.10.1.108 SMRU Finance office Wararat 64-bit Windows 10 Enterprise 1809 Online oh Microsoft Windows [Version 10.0.17763.107] SMRUWS-AD22 10.10.1.152 SMRU Meeting room SMRU 64-bit Windows 10 Enterprise 1809 Online Microsoft Windows [Version 10.0.17763.107]

SMRUWS-IT10\Administrator C:\Users\Administrator# powershell -ExecutionPolicy Bypass -File "C:\Tmp\Remote-Fail.ps1" Show-Status Computer IP Address Domain Location User OS Arch OS Name Status Logged on users Reply


SMRUWS-AD01 10.10.1.79 SMRU ANC office Mookhopaw 64-bit Windows 10 Enterprise 1809 Offline SMRUWS-AD04 SMRU Data office Miasa 64-bit Windows 7 Professional Ssh SMRUWS-AD05 10.10.1.75 SMRU Finance office Amorn 64-bit Windows 7 Professional Offline SMRUWS-AD06 10.10.1.109 SMRU Finance office Kee 64-bit Windows 7 Professional Offline SMRUWS-AD07 10.10.1.122 SMRU Admin office Sittipong 64-bit Windows 7 Professional Offline SMRUWS-AD11 SMRU METF office 2 Micho 64-bit Windows 10 Enterprise 1809 Unknown SMRUWS-AD12 SMRU Pharmacy room Carela 64-bit Windows 7 Professional Ssh SMRUWS-AD14 SMRU Finance office Jeerawan.w 64-bit Windows 7 Professional Unknown SMRUWS-AD15 SMRU Logistics Siam 64-bit Windows 7 Professional Ssh SMRUWS-AD17 SMRU Finance office Janjira 64-bit Windows 7 Professional Ssh SMRUWS-AD18 SMRU Staff office Sangrawee 64-bit Windows 7 Professional Ssh SMRUWS-AD19 SMRU HR Suchadas 64-bit Windows 8.1 Enterprise Ssh SMRUWS-AD20 SMRU Grant Phaitoon 64-bit Windows 8.1 Enterprise Ssh SMRUWS-AD21 SMRU Finance office Wararat 64-bit Windows 10 Enterprise 1809 Ssh SMRUWS-AD22 SMRU Meeting room SMRU 64-bit Windows 10 Enterprise 1809 Ssh

SMRUWS-IT10\Administrator C:\Users\Administrator# powershell -ExecutionPolicy Bypass -File "C:\Tmp\Remote-Fail.ps1" Show-Status Computer IP Address Domain Location User OS Arch OS Name Status Logged on users Reply


SMRUWS-AD04 10.10.1.80 SMRU Data office Miasa 64-bit Windows 7 Professional Online miasa Microsoft Windows [Version 6.1.7601]

SMRUWS-IT10\Administrator C:\Users\Administrator# powershell -ExecutionPolicy Bypass -File "C:\Tmp\Remote-Fail.ps1" Show-Status Computer IP Address Domain Location User OS Arch OS Name Status Logged on users Reply


SMRUWS-AD04 SMRU Data office Miasa 64-bit Windows 7 Professional Dns Host key verification failed. SMRUWS-AD15 SMRU Logistics Siam 64-bit Windows 7 Professional Dns Host key verification failed. SMRUWS-AD21 SMRU Finance office Wararat 64-bit Windows 10 Enterprise 1809 Ssh SMRUWS-AD22 SMRU Meeting room SMRU 64-bit Windows 10 Enterprise 1809 Dns Host key verification failed.

SMRUWS-IT10\Administrator C:\Users\Administrator# powershell -ExecutionPolicy Bypass -File "C:\Tmp\Remote-Fail.ps1" Show-Status Computer IP Address Domain Location User OS Arch OS Name Status Logged on users Reply


SMRUWS-AD04 SMRU Data office Miasa 64-bit Windows 7 Professional Ssh SMRUWS-AD15 SMRU Logistics Siam 64-bit Windows 7 Professional Ssh SMRUWS-AD21 SMRU Finance office Wararat 64-bit Windows 10 Enterprise 1809 Ssh SMRUWS-AD22 SMRU Meeting room SMRU 64-bit Windows 10 Enterprise 1809 Ssh

SMRUWS-IT10\Administrator C:\Users\Administrator# powershell -ExecutionPolicy Bypass -File "C:\Tmp\Remote-Fail.ps1" Show-Status Computer IP Address Domain Location User OS Arch OS Name Status Logged on users Reply


SMRUWS-AD04 10.10.1.80 SMRU Data office Miasa 64-bit Windows 7 Professional Online miasa Microsoft Windows [Version 6.1.7601] SMRUWS-AD15 10.10.1.84 SMRU Logistics Siam 64-bit Windows 7 Professional Online siam Microsoft Windows [Version 6.1.7601] SMRUWS-AD21 10.10.1.108 SMRU Finance office Wararat 64-bit Windows 10 Enterprise 1809 Online oh Microsoft Windows [Version 10.0.17763.107] SMRUWS-AD22 10.10.1.152 SMRU Meeting room SMRU 64-bit Windows 10 Enterprise 1809 Online Microsoft Windows [Version 10.0.17763.107]

SMRUWS-IT10\Administrator C:\Users\Administrator# powershell -ExecutionPolicy Bypass -File "C:\Tmp\Remote-Fail.ps1" Show-Status Computer IP Address Domain Location User OS Arch OS Name Status Logged on users Reply


SMRUWS-AD04 10.10.1.80 SMRU Data office Miasa 64-bit Windows 7 Professional Online miasa Microsoft Windows [Version 6.1.7601] SMRUWS-AD15 10.10.1.84 SMRU Logistics Siam 64-bit Windows 7 Professional Online siam Microsoft Windows [Version 6.1.7601] SMRUWS-AD21 10.10.1.108 SMRU Finance office Wararat 64-bit Windows 10 Enterprise 1809 Online oh Microsoft Windows [Version 10.0.17763.107] SMRUWS-AD22 10.10.1.152 SMRU Meeting room SMRU 64-bit Windows 10 Enterprise 1809 Online Microsoft Windows [Version 10.0.17763.107]

SMRUWS-IT10\Administrator C:\Users\Administrator# powershell -ExecutionPolicy Bypass -File "C:\Tmp\Remote-Fail.ps1" Show-Status Computer IP Address Domain Location User OS Arch OS Name Status Logged on users Reply


SMRUWS-AD04 SMRU Data office Miasa 64-bit Windows 7 Professional Ssh SMRUWS-AD15 SMRU Logistics Siam 64-bit Windows 7 Professional Ssh SMRUWS-AD21 SMRU Finance office Wararat 64-bit Windows 10 Enterprise 1809 Ssh SMRUWS-AD22 SMRU Meeting room SMRU 64-bit Windows 10 Enterprise 1809 Ssh