PowerShell script to check if process is running on multiple remote computers and copy a file if process is NOT running, keep list of those which succeeded etc

Darren Rose 496 Reputation points
2021-01-22T18:48:13.893+00:00

Hi, I would like some advice/help on improving the script below which was written with help on another forum.

I will explain the requirements first so you can see what I am doing.

  • To copy a file to lots of computers on domain network, but file can only be copied if specific process (app) is not running

So the steps I need to do:-

1) Check if computer is online or accessible - I used Test-Connection for this as seemed to work okay - but had to add -ErrorAction SilentlyContinue if not got error "Test-Connection : Testing connection to computer 'Tfdffdd' failed: No such host is known" for computers which didn't exist in DNS etc. Store failure computer namesin a variable to try later, if pass then continue - NOTE: Perhaps I should use -Quiet here to just get back a boolean result?

2) I then need to check I can actually access the computer using PS Remoting as had some cases where get error below due to firewall config or because they are on a public network - these are things I can easily fix when I know which computers are affected - but no point in trying to copy a file if can't even connect etc - so handy to store failure computer names in another variable which I can sort later, and if connect okay to continue

[Lxxxx] Connecting to remote server Lxxxx failed with the following error message : WinRM cannot process the request. The following error with errorcode 0x80090322 occurred while using Kerberos authentication: An unknown security error occurred. Possible causes are: -The user name or password specified are invalid. -Kerberos is used when no authentication method and no user name are specified. -Kerberos accepts domain user names, but not local user names. -The Service Principal Name (SPN) for the remote computer name and port does not exist. -The client and remote computers are in different domains and there is no trust between the two domains. After checking for the above issues, try the following: -Check the Event Viewer for events related to authentication. -Change the authentication method; add the destination computer to the WinRM TrustedHosts configuration setting or use HTTPS transport. Note that computers in the TrustedHosts list might not be authenticated. -For more information about WinRM configuration, run the following command: winrm help config. For more information, see the about_Remote_Troubleshooting Help topic. + CategoryInfo : OpenError: (Lxxxx:String) [], PSRemotingTransportException + FullyQualifiedErrorId : -2144108387,PSSessionStateBroken

For this I had tried Test-WSMan but that comes back as all okay even though I can't connect to them, so instead I used Invoke-Command which worked fine

3) I then check to see if a specifc process is running - using Get-Process which works fine - if process is running then store computer name in variable to try again later, if process is not running then continue

4) Copy file to computer - for this Copy-Item worked fine - BUT on the odd computer (down to a current DNS issue) I get an error below which I need to handle somehow as with current script it thinks it is a success wrongly

Copy-Item : The network path was not found
At C:\Tmp\PCA_TESTING_ONLY\file_copy.ps1:11 char:37

  • ... =$computer; Copy-Item -Path "\lb-nordc\SoftwareInstalls\CCH_ProAudit ...
  • ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  • CategoryInfo : NotSpecified: (:) [Copy-Item], IOException
  • FullyQualifiedErrorId : System.IO.IOException,Microsoft.PowerShell.Commands.CopyItemCommand

5) show results based on which of the four stages it got to - e.g. offiline, remoting failed, process running, file copied okay

Below are two version of the scripts I have been using.

I am still learning so would like any comments / advice on

a) how the scripts can be made better - any other methods I could use e.g. try..catch to make it better / more bulletproof?

b) any poor scripting practices etc

c) how to combine the two checks e.g. Test-Connection first and then check can access remotely second before continuing (point 1 and 2 above) - or is the second bit an area for try..catch perhaps?

d) how to handle file copy-item error in point 4 above

Anything I have missed please ask.

Much appreciated in advance for any feedback / help given :)

Script 1 - just checking if online using Test-Connection

$computers = @("PC1", "PC3")
$RNote = @()
$NNote = @()
$off = @()

Foreach ($computer in $computers) {
    $TestC = Test-Connection -ComputerName $computer -Count 1 -ErrorAction SilentlyContinue 
    If (!($TestC)) { $off += $computer } Else {

        $Procs = Invoke-Command -ComputerName $computer { Get-Process Notepad -ErrorAction SilentlyContinue }
        If (!$Procs) { $NNote += $computer; Copy-Item -Path "\\lserver\myfile.txt" -Destination "\\$computer\c$\folder\" -Force }
        elseif ($Procs) { $RNote += $computer }
    }
}

$leng = [array]$RNote.count, $NNote.Count, $off.count
[int]$max = ($leng | Measure-Object -Maximum).Maximum
for ($i = 0; $i -lt $max; $i++) {
    [pscustomobject]@{
        "Notepad On"  = $(if ($RNote[$i]) { $RNote[$i] })
        "Notepad Off" = $(if ($NNote[$i]) { $NNote[$i] })
        "Offline "    = $(if ($off[$i]) { $off[$i] })
    }
}  

Script 2 - checking if can access using ps remoting

$computers = @("NOa-IT")
$RNote = @()
$NNote = @()
$off = @()

Foreach ($computer in $computers) {
    $TestRemote = Invoke-Command -ComputerName $computer { 1 } -ErrorAction SilentlyContinue
    If (($TestRemote -ne 1)) { $off += $computer } Else {

        $Procs = Invoke-Command -ComputerName $computer { Get-Process Proaudit -ErrorAction SilentlyContinue }
        If (!$Procs) { $NNote += $computer; Copy-Item -Path "\\server\myfile.txt" -Destination "\\$computer\c$\folder\" -Force }
        elseif ($Procs) { $RNote += $computer }
    }
}

$leng = [array]$RNote.count, $NNote.Count, $off.count
[int]$max = ($leng | Measure-Object -Maximum).Maximum
for ($i = 0; $i -lt $max; $i++) {
    [pscustomobject]@{
        "File NOT Copied" = $(if ($RNote[$i]) { $RNote[$i] })
        "File Copied"     = $(if ($NNote[$i]) { $NNote[$i] })
        "Offline "        = $(if ($off[$i]) { $off[$i] })
    }
}  
Windows Server PowerShell
Windows Server PowerShell
Windows Server: A family of Microsoft server operating systems that support enterprise-level management, data storage, applications, and communications.PowerShell: A family of Microsoft task automation and configuration management frameworks consisting of a command-line shell and associated scripting language.
5,462 questions
0 comments No comments
{count} votes

Accepted answer
  1. Rich Matheisen 45,906 Reputation points
    2021-01-22T22:05:08.643+00:00

    Ignoring, for the moment, the "best" way to test for connectivity, or WinRM (or both) connectivity, if you have a lot of machine that are going to be the subject of that script, you can speed thing up by running things in parallel with Invoke-Command.

    Try this on a small set of computers and see if it works in the same way as your "Script 1":

    $computers = @("PC1", "PC3")
    $Results = @()
    # get online machines
    $TestC = Test-Connection -Computer $computers -Count 2 -ErrorAction SilentlyContinue -WarningAction SilentlyContinue |
                Select-Object -ExpandProperty Address
    # get offline machines
    $Computers |
        ForEach-Object{
            if ($TestC -notcontains $_){
                $Results += [PSCustomObject]@{
                    Computer = $_
                    "Notepad On" = "N/A"
                    "Notepad Off" = "N/A"
                    "Offline" = $true
                }
                $_
            }
        }
    # see if notepad is running on online machines
    $Procs = Invoke-Command -ComputerName $TestC { Get-Process Notepad -ErrorAction SilentlyContinue }
    $Procs |
        ForEach-Object{
            $Results += [PSCustomObject]@{
                            Computer = $_.PSComputerName
                            "NotePad On" = $true
                            "Notepad Off" = $false
                            "OffLine" = $false
                        }
        }
    # copy file to machine if not running notepad
    $xProcs = $Procs | Select-Object -ExpandProperty PSComputerName
    $TestC |
        ForEach-Object{
            if ($xProcs -notcontains $_){
                $Results += [PSCustomObject]@{
                    Computer = $_
                    "NotePad On" = $false
                    "Notepad Off" = $true
                    "OffLine" = $false
                }
                Copy-Item -Path "\\lserver\myfile.txt" -Destination "\\$_\c$\folder\" -Force
            }
        }
    $Results
    

2 additional answers

Sort by: Most helpful
  1. Rich Matheisen 45,906 Reputation points
    2021-01-23T03:32:03.143+00:00

    This should "parallelize" the copying. Test it first!

    $computers = @("PC1", "PC3")
    $Results = @()
    # get online machines
    $TestC = Test-Connection -Computer $computers -Count 2 -ErrorAction SilentlyContinue -WarningAction SilentlyContinue |
                Select-Object -ExpandProperty Address -Unique
    # get offline machines
    $Computers |
        ForEach-Object{
            if ($TestC -notcontains $_){
                $Results += [PSCustomObject]@{
                    Computer = $_
                    "Notepad On" = "N/A"
                    "Notepad Off" = "N/A"
                    "Offline" = $true
                    Copied = "N/A"
                }
                $_
            }
        }
    # see if notepad is running on online machines
    $Procs = Invoke-Command -ComputerName $TestC { Get-Process Notepad -ErrorAction SilentlyContinue }
    $Procs |
        ForEach-Object{
            $Results += [PSCustomObject]@{
                            Computer = $_.PSComputerName
                            "NotePad On" = $true
                            "Notepad Off" = $false
                            "OffLine" = $false
                            Copied = "N/A"
                        }
        }
    # copy file to machine if not running notepad
    $xProcs = $Procs | Select-Object -ExpandProperty PSComputerName
    $TestC |
        ForEach-Object{
            if ($xProcs -notcontains $_){
                [array]$RunOn += $_
            }
    $RunOn |
        ForEach-Object{
            $Jobs += Start-Job -Name "CopyTo-$_" -ScriptBlock{
                        param (
                            [String]$Machine
                        )
                        Try{
                            Copy-Item -Path "\\lserver\myfile.txt" -Destination "\\$Machine\c$\folder\" -Force -ErrorAction Stop | Out-Null
                            $true
                        }
                        Catch{
                            $false
                        }
                    } -ArgumentList $_
            $Jobs | 
                Wait-Job |
                    Receive-Job |
                        ForEach-Object{
                            $Results += [PSCustomObject]@{
                                Computer = $_
                                "NotePad On" = $false
                                "Notepad Off" = $true
                                "OffLine" = $false
                                Copied = $_
                            }
                        }
            $Jobs | 
                Remove-Job | 
                    Out-Null
        }
    $Results
    
    1 person found this answer helpful.

  2. Rich Matheisen 45,906 Reputation points
    2021-01-29T20:08:39.383+00:00

    Here's a script that checks for "ping" status, WinRM status, and then offloads the check for a running process and file transfer to the remote machine. The results are sent to a CSV.

    It looks larger than it really is. A lot of the code is checking/recording success/failure.

    $computers = @("WS05", "WS06", "WONKY")
    $Results = @{} # a hash of hashes, key = machine name
    
    # test all machines for online status
    # may not be entirely necessary but may help with troubleshooting
    Test-Connection -Computer $computers -Count 2 2>&1 |
        ForEach-Object{
            $State = [ordered]@{
                Machine = ""
                Pingable = ""
                PingError = ""
                WinRM = "N/A"
                WinRMError = ""
                AppRunning = "N/A"
                FileCopied = "N/A"
                ExecError = ""
            }
            If ($_ -is [System.Management.Automation.ErrorRecord]){
                # only record failure if there have been no other successes
                # ICMP is an unreliable protocol and success and failures may
                # be intermingled. A failure followed by a success means the
                # machine is reachable and responding. After a success don't
                # record any failure
                if (-not $results.ContainsKey($_.TargetObject)){
                    $State.Machine = $_.TargetObject
                    $State.Pingable = $false
                    $State.PingError = $_.Exception
                    $Results.($_.TargetObject) = $State
                }
            }
            elseif($_.pstypenames -contains 'System.Management.ManagementObject#root\cimv2\Win32_PingStatus'){
                $State.Machine = $_.Address
                $State.Pingable = $true
                $State.PingError = ""
                $Results.($_.Address) = $State
            }
            else{
                "Uh-Oh" # shouldn't happen -- maybe throw exception to see why??
            }
        }
    
    # Check each machine for WinRM, process running, and copy-item
    # Do this for ALL machines regardless of ping failures becasue
    # a failure to ping doesn't mean WinRM will fail too
    Invoke-Command -ComputerName $computers -ScriptBlock{
        $x = [PSCustomObject]@{
                AppRunning = $false
                FileCopied = $false
                ExecError = ""
            }
        Try{
            if (Get-Process Notepad -ErrorAction Stop){
                $x.AppRunning = $true
            }
        }
        Catch{
            if (-not $_.Exception -like "Cannot find a process with the name*"){
                $x.ExecError = $_.ToString()
            }
        }
        if ( (-not $x.AppRunning) -and $x.ExecError.Length -eq 0 ){
            Try{
                Copy-Item -Path "\\lserver\myfile.txt" -Destination "c:\folder\" -Force -ErrorAction Stop | Out-Null
                $x.FileCopied = $true
            }
            Catch{
                $x.ExecError = $_.ToString()
            }
        }
        $x
    } 2>&1 |
        ForEach-Object{
            If ($_ -is [System.Management.Automation.ErrorRecord]){
                $Results.($_.TargetObject).WinRM = $false
                $Results.($_.TargetObject).WinRMError = $_.Exception
            }
            elseif($_ -is [PSCustomObject]){
                $Results.($_.PSComputerName).WinRM = $true
                $Results.($_.PSComputerName).ExecError = $_.ExecError
                $Results.($_.PSComputerName).AppRunning = $_.AppRunning
                $Results.($_.PSComputerName).FileCopied = $_.FileCopied
            }
        }
    $Results.GetEnumerator()|
        ForEach-Object{
            [PSCustomObject]$_.Value
        } | Export-CSV c:\junk\Whatever.csv -NoTypeInformation
    
    1 person found this answer helpful.