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 for business | Windows Server | User experience | PowerShell
0 comments No comments
{count} votes

Answer accepted by question author
  1. Rich Matheisen 48,026 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 48,026 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 48,026 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.

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.