Getting Processes via Get-Counter does not refresh when processes change and throws errors if processes end - powershell

My script monitors the CPU usage of the process, looping the code every 5 sec and writing it to a file. Which works fine.
But I found when a new Process runs my script will not find it until I stop the script and rerun it again.
Also if a process ends/stops, the script give an this error:
Get-Counter : The data in one of the performance counter samples is
not valid. View the Status property for each
PerformanceCounterSample object to make sure it contains valid data.
At line:2 char:34
It seems PowerShell retrieves the Process information only once and caches it.
If I run the bellow script (which is a part of all my script), it runs perfectly:
while($true) {
$ProcessId = (Get-Counter "\Process(*)\ID Process").CounterSamples
$ProcessId.count
Start-Sleep -s 5
}
If I have 50 process it will gives 50, but if a new process starts it will keep giving 50 until I restart the script.
If I stop any process it will give the same error above.
Any idea how to solve this problem and force PowerShell to reread the process list without restarting the script?

You could use PowerShell Jobs to execute it in a new background process on each iteration and use -ErrorAction SilentlyContinue to suppress the error messages that might occur if one or more processes stopped during a check:
while($true) {
$ProcessId = Start-Job -ScriptBlock { (Get-Counter "\Process(*)\ID Process" -ErrorAction SilentlyContinue).CounterSamples } | Wait-Job | Receive-Job
$ProcessId.Count
Start-Sleep -Seconds 5
}

Related

PowerShell Remoting, Eventing, Runspace - sticky Event Subscriber

I am currently stuck at a problem involving a WMI Event subscriber on a remote session running in a background runspace.
Below is the representative code that reproduces the issue (the real code is too long to share here) but, essentially, through a PS script I remotely install advertised WSUS updates with reboots when necessary. It takes about 90 minutes end to end.
The issue I am trying to solve at the moment is that during the course of patching, the support staff inadvertently log in to the the server being remotely patched and do their admin activities. To remind them, I want to display a pop up message as soon as the user logs in when my remote script is running. I am trying to do it using a background runspace plugged into the main patching script. It makes use of WMI eventing on the target server (which is being patched) to monitor user logons and display message as soon as it detects one. The below code is working as I expect. It even survives target server reboots.
$RemoteServerName = 'Server1.contoso.com'
$UserLogonAlertScriptBlock = {
param ($SyncedHashTable)
$RemoteServerName = $SyncedHashTable.target
try {
$Session = New-PSSession -ComputerName $RemoteServerName -ErrorAction Stop
} catch {}
while($true){
if($Session.State -eq 'Opened') {
$RemoteMonitoringJob = Invoke-Command -Session $Session -AsJob -ScriptBlock {
$null = Register-WMIEvent -Query "SELECT * FROM __InstanceCreationEvent WITHIN 3 WHERE TargetInstance ISA 'Win32_LogonSession'" -SourceIdentifier 'User.Logon'
Wait-Event -SourceIdentifier "User.Logon" -Timeout 7200 | ForEach-Object {
msg * /TIME:7200 /V "User logon detected" | Out-Null
$_ | Remove-Event
}
}
while($RemoteMonitoringJob.State -in #('NotStarted','Running')) {
Start-Sleep -Seconds 1
}
} else {
while($true) {
try {
$Session = New-PSSession -ComputerName $RemoteServerName -ErrorAction Stop
} catch {}
if($Session.State -eq 'Opened') {
break
}
Start-Sleep -Seconds 1
}
}
}
}
$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.runspace = $Runspace
$SyncedHashTable = [hashtable]::Synchronized(#{})
$SyncedHashTable.host = $host
$SyncedHashTable.target = $RemoteServerName
$Runspace.Open()
$handle = $PowerShell.AddScript($UserLogonAlertScriptBlock).AddArgument($SyncedHashTable).BeginInvoke()
Write-Host '$(Get-Date): Long running script execution targeting $RemoteServerName has started'
Start-Sleep -Seconds 120 # it usually runs for upto 90 minutes, with remote reboots of $RemoteServerName
Write-Host "$(Get-Date): The script execution has completed"
### The code that cleans up the sticky event subscriber on the target server needs to be added here
The part I am stuck at is after the script completes its execution. The wsmanprovhost.exe running on the target server continues to stick around and shows alert messages when new users log on. I think it's because of the WMI event listener still being active on the box, not releasing the remote PS session.
In the above code, I need help close that remote listener so wsmanprovhost.exe disappears.
Could you please help?
PS. I have referred to #mklement0 's response in the following post but still no joy: The RunSpace and its closure
Update:
I have managed to address the challenge by adding a Boolean flag into the SyncedHashtable which is passed to the background runspace. When I want to stop the remote logon monitoring, in the main script I flip the flag. Since it's inside a synced hashtable, I can monitor that inside the runspace and terminate the remote invoke command job in the run space. But I still had to forcibly kill the remote wsmprovhost.exe as it refuses to go. I could do it by getting the pid of the remote PS session in advance. Not the most elegant way to close a remote PS session but it does the job for me. It's just that since the remote session is continuously monitoring for user logon event, there does not appear to be a way to run a piece of code in that session to unsubscribe the WMI event source. Will do more testing to see if there is any side effect.

Powershell Script cycles through machines but hangs if one loses network temporarily

I have a powershell script that parses a txt file which is full of machine names, then one by one, it creates a session to the system, runs a few commands, and moves to the next system. The script usually take about 10-30 seconds to run on each system depending on the case encountered in the script.
Once in a while the system that is currently being checked will lose the network connection for some various reason. When this happens the console starts writing yellow warning messages about attempting to reconnect for 4 minutes and then disconnects the session when it cannot reconnect.
Even if it establishes the connection again within the 4 minutes, it doesn't do anything after that, it's like the script just freezes. It won't move on to the next system and it doesn't stop the script, I have to manually stop it, or if i manually run the script, i can hit control+c to break out of the current loop, and it then moves on to the next machine in the list.
Is there any way to break out of the current loop if a warning is encountered so it can move on to the next machine? That would be my ideal solution. thanks!
Script is simple..
foreach($server in Get-Content .\machines.txt) {
if($server -match $regex){
invoke-command $server -ErrorAction SilentlyContinue -ScriptBlock{
command1
command2
command3
}
}
this is what happens
PS C:\temp> .\script.ps1
machine1
machine2
machine3
machine4
machine5
WARNING: The network connection to machine5 has been interrupted. Attempting to reconnect for up to 4 minutes...
WARNING: Attempting to reconnect to machine5 ...
WARNING: Attempting to reconnect to machine5 ...
WARNING: Attempting to reconnect to machine5 ...
WARNING: The network connection to machine5 has been restored.
But it never goes on to machine6
When i work remotely with multiple machines i usually start the processes on the machines in parallel. So i have less impact when single machines are timing out. I use powershell 7 ForEach-Object -Parallel Feature for this https://devblogs.microsoft.com/powershell/powershell-foreach-object-parallel-
feature/
Try something like this:
$Credential=Get-Credential
#all Necessary parameters must be in the Object i give to ForEach Object
$myHosts = #(
#Hosts i want to connect to with values i want to use in the loop
#{Name="probook";cred=$Credential;param1="one_1";param2="two_1"}
#{Name="probook";cred=$Credential;param1="one_2";param2="two_2"}
)
$var1="one"
$var2="two"
$myHosts | ForEach-Object -Parallel {
#Variables outside of this "Parallel" Loop are not available. Because this is startet as separate SubProcess
#All Values come from the Object i piped in the ForEach-Object
$myHost=$_
#This is written to your local Shell
Write-Host ("Computer: "+ $env:Computername)
Write-Host $myHost.param1
Write-Host $myHost.param2
Write-Host $myHost.cred.UserName
Invoke-Command -ComputerName $myHost.Name -Credential $myHost.cred -ArgumentList #($myHost.param1,$myHost.param2) -ScriptBlock {
#Variables outside of of this Invoke Command Script Block are not available because this is a new Remote-Shell on the remote Host
#Parameters in Ordner of -Argument List
param($param1,$param2)
#Do your things on the Remote-Host here
#This is not Visbible -> it is only written on the "remote Shell"
Write-Host $env:Computername
#Here you get Back Values from the remote Shell
$env:Computername
$param1
$param2
}
} -ThrottleLimit 5
Hmm his is indeed a Problem.
You could experiment with:
Start-Job
(https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/start-job?view=powershell-7.1)
Get-Job (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-job?view=powershell-7.1)
Receive-Job (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/receive-job?view=powershell-7.1)
So you have more control what the processes do.
You start Background Jobs with Start-Job. Start-Job deliveres Job Objects Back -> save them in a array or variables
With Get-Job you see all Jobs currently Running
with Receive-Job you get back the output from a Job so far. You can use receive-Job to get back all PSObjects a Background Job has written.
Cannot explain in Detail, but this woul be another try i would do for this job.

Getting specific app pool's worker process in PowerShell returns value but process is already stopped

I have multiple websites - each on a separate app pool.
The app pool I'm referring to has 1 worker process.
After stopping the app pool, I'm trying to wait and verify that the worker process has stopped.
$appPoolName = $appPool.name;
Write-Host "appPoolName: $appPoolName";
$w3wp = Get-ChildItem "IIS:\AppPools\$appPoolName\WorkerProcesses\";
while($w3wp -and $retrys -gt 0)
{
Write-Host "w3wp value is: $w3wp";
Start-Sleep -s 10;
$retrys--;
$w3wp = Get-ChildItem "IIS:\AppPools\$appPoolName\WorkerProcesses\";
Write-Host "w3wp value(2) is: $w3wp";
if(-not $w3wp)
{
break;
}
}
The print of both values is always "Microsoft.IIs.PowerShell.Framework.ConfigurationElement", even when I see the process is stopped and no longer in Task Manager.
Also strange: When I open another PowerShell session while the code runs and call
$w3wp = Get-ChildItem "IIS:\AppPools\$appPoolName\WorkerProcesses\";
w3wp has no value (because it is no longer exist).
Any ideas why the value isn't changing?
Or maybe how to do that differently?
Thanks in advance :)
I think the IIS: provider is caching data. I dont know of a fix, but heres a couple of alternatives:
use WMI from powershell:
gwmi -NS 'root\WebAdministration' -class 'WorkerProcess' | select AppPoolName,ProcessId
Run appcmd
appcmd list wp

Do threads still execute using -asjob with wait-job?

Hello all and good afternoon!
I had a quick question regarding -asjob running with invoke-command.
If I run 2 Invoke-Command's using -asjob, does it run simultaneously when I try to receive the ouput? Does this mean wait-job waits till the first job specified is finished running to get the next results?
Write-Host "Searching for PST and OST files. Please be patient!" -BackgroundColor White -ForegroundColor DarkBlue
$pSTlocation = Invoke-Command -ComputerName localhost -ScriptBlock {Get-Childitem "C:\" -Recurse -Filter "*.pst" -ErrorAction SilentlyContinue | % {Write-Host $_.FullName,$_.lastwritetime}} -AsJob
$OSTlocation = Invoke-Command -ComputerName localhost -ScriptBlock {Get-Childitem "C:\Users\me\APpdata" -Recurse -Filter "*.ost" -ErrorAction SilentlyContinue | % {Write-Host $_.FullName,$_.lastwritetime} } -AsJob
$pSTlocation | Wait-Job | Receive-Job
$OSTlocation | Wait-Job | Receive-Job
Also, another question: can i save the output of the jobs to a variable without it showing to the console? Im trying to make it where it checks if theres any return, and if there is output it, but if theres not do something else.
I tried:
$job1 = $pSTlocation | Wait-Job | Receive-Job
if(!$job1){write-host "PST Found: $job1"} else{ "No PST Found"}
$job2 = $OSTlocation | Wait-Job | Receive-Job
if(!$job2){write-host "OST Found: $job2"} else{ "No OST Found"}
No luck, it outputs the following:
Note: This answer does not directly answer the question - see the other answer for that; instead, it shows a reusable idiom for a waiting for multiple jobs to finish in a non-blocking fashion.
The following sample code uses the child-process-based Start-Job cmdlet to create local jobs, but the solution equally works with local thread-based jobs created by Start-ThreadJob as well as jobs based on remotely executing Invoke-Command -ComputerName ... -AsJob commands, as used in the question.
It shows a reusable idiom for a waiting for multiple jobs to finish in a non-blocking fashion that allows for other activity while waiting, along with collecting per-job output in an array.
Here, the output is only collected after each job completes, but note that collecting it piecemeal, as it becomes available, is also an option, using (potentially multiple) Receive-Job calls even before a job finishes.
# Start two jobs, which run in parallel, and store the objects
# representing them in array $jobs.
# Replace the Start-Job calls with your
# Invoke-Command -ComputerName ... -AsJob
# calls.
$jobs = (Start-Job { Get-Date; sleep 1 }),
(Start-Job { Get-Date '1970-01-01'; sleep 2 })
# Initialize a helper array to keep track of which jobs haven't finished yet.
$remainingJobs = $jobs
# Wait iteratively *without blocking* until any job finishes and receive and
# output its output, until all jobs have finished.
# Collect all results in $jobResults.
$jobResults =
while ($remainingJobs) {
# Check if at least 1 job has terminated.
if ($finishedJob = $remainingJobs | Where State -in Completed, Failed, Stopped, Disconnected | Select -First 1) {
# Output the just-finished job's results as part of custom object
# that also contains the original command and the
# specific termination state.
[pscustomobject] #{
Job = $finishedJob.Command
State = $finishedJob.State
Result = $finishedJob | Receive-Job
}
# Remove the just-finished job from the array of remaining ones...
$remainingJobs = #($remainingJobs) -ne $finishedJob
# ... and also as a job managed by PowerShell.
Remove-Job $finishedJob
} else {
# Do other things...
Write-Host . -NoNewline
Start-Sleep -Milliseconds 500
}
}
# Output the jobs' results
$jobResults
Note:
It's tempting to try $remainingJobs | Wait-Job -Any -Timeout 0 to momentarily check for termination of any one job without blocking execution, but as of PowerShell 7.1 this doesn't work as expected: even already completed jobs are never returned - this appears to be bug, discussed in GitHub issue #14675.
If I run 2 Invoke-Command's using -asjob, does it run simultaneously when I try to receive the output?
Yes, PowerShell jobs always run in parallel, whether they're executing remotely, as in your case (with Invoke-Command -AsJob, assuming that localhost in the question is just a placeholder for the actual name of a different computer), or locally (using Start-Job or Start-ThreadJob).
However, by using (separate) Wait-Job calls, you are synchronously waiting for each jobs to finish (in a fixed sequence, too). That is, each Wait-Job calls blocks further execution until the target job terminates.[1]
Note, however, that both jobs continue to execute while you're waiting for the first one to finish.
If, instead of waiting in a blocking fashion, you want to perform other operations while you wait for both jobs to finish, you need a different approach, detailed in the the other answer.
can i save the output of the jobs to a variable without it showing to the console?
Yes, but the problem is that in your remotely executing script block ({ ... }) you're mistakenly using Write-Host in an attempt to output data.
Write-Host is typically the wrong tool to use, unless the intent is to write to the display only, bypassing the success output stream and with it the ability to send output to other commands, capture it in a variable, or redirect it to a file. To output a value, use it by itself; e.g., $value instead of Write-Host $value (or use Write-Output $value, though that is rarely needed); see this answer.
Therefore, your attempt to collect the job's output in a variable failed, because the Write-Host output bypassed the success output stream that variable assignments capture and went straight to the host (console):
# Because the job's script block uses Write-Host, its output goes to the *console*,
# and nothing is captured in $job1
$job1 = $pSTlocation | Wait-Job | Receive-Job
(Incidentally, the command could be simplified to
$job1 = $pSTlocation | Receive-Job -Wait).
[1] Note that Wait-Job has an optional -Timeout parameter, which allows you to limit waiting to at most a given number of seconds and return without output if the target job hasn't finished yet. However, as of PowerShell 7.1, -Timeout 0 for non-blocking polling for whether jobs have finished does not work - see GitHub issue #14675.

Web service call, if application is running

I'm looking for a way to execute a web form submittal if an application is running. I'm not sure the best approach, but I did create a PowerShell script that accomplishes what I want.
while($true) {
(Invoke-WebRequest -Method post 'Http://website.com').Content;
Start-Sleep -Seconds 600;
}
Now what I'd like to do is run this only when an application is running and then quit if the application is no longer running. I suspect maybe a Windows service would be the answer? If so, any idea how I could accomplish this?
I had also thought about running this as a Google Chrome extension, but then my googlefu was exhausted. For Chrome, I would just need the script and no need to check on the .exe.
Any thoughts or help would be appreciated. Again, I'm way out of my depth here but have found a need to create something so dummy steps would be much desired.
If you know the name of the process that runs for the application, you can do the following:
$processname = "thing"
# Wait until the process is detected
Do {
Sleep 60
} Until (Get-Process $processName)
# Once it is detected, run the script
# < SCRIPT RUN CODE HERE >
While (1) {
# Monitor the process to make sure it is still running
If (Get-Process $processName) {
Continue
}
Else {
# Stop the script, because the process isn't running.
# < SCRIPT STOP CODE HERE >
# Wait until the process is detected again
Do {
Sleep 60
} Until (Get-Process $processName)
# Once it is detected again, run the script
# < SCRIPT RUN CODE HERE >
}
# You can add in a delay here to slow down the loop
# Sleep 60
}
I think what you're looking for might be WMI eventing. You can register for (and respond to) events that occur within WMI, such as:
When a process starts or stops
When a service starts or stops
When a process exceeds a certain amount of memory usage
When a new version device driver is installed
When a computer is assigned to a new organizational unit
When a user logs on or off
When an environment variables changes
When a laptop battery drops below a certain threshold
Thousands of other cases
To register for WMI events, use the Register-WmiEvent cmdlet. You can use the -Action parameter to declare what PowerShell statements to execute when a matching event is detected. Here is a simple example:
# 1. Start notepad.exe
notepad;
# 2. Register for events when Notepad disappears
# 2a. Declare the WMI event query
$WmiEventQuery = "select * from __InstanceDeletionEvent within 5 where TargetInstance ISA 'Win32_Process' and TargetInstance.Name = 'notepad.exe'";
# 2b. Declare the PowerShell ScriptBlock that will execute when event is matched
$Action = { Write-Host -ForegroundColor Green -Object ('Process stopped! {0}' -f $event.SourceEventArgs.NewEvent.TargetInstance.Name) };
# 2c. Register for WMI events
Register-WmiEvent -Namespace root\cimv2 -Query $WmiEventQuery -Action $Action -SourceIdentifier NotepadStopped;
# 3. Stop notepad.exe
# Note: For some reason, if you terminate the process as part of the same thread, the event
# doesn't seem to fire correctly. So, wrap the Stop-Process command in Start-Job.
Start-Job -ScriptBlock { Stop-Process -Name notepad; };
# 4. Wait for event consumer (action) to fire and clean up the event registration
Start-Sleep -Seconds 6;
Unregister-Event -SourceIdentifier NotepadStopped;
FYI: I developed a PowerShell module called PowerEvents, which is hosted on CodePlex. The module includes the ability to register permanent WMI event subscriptions, and includes a 30+ page PDF document that helps you to understand WMI eventing. You can find this open-source project at: http://powerevents.codeplex.com.
If I were to adapt your code to something that is more practical for you, it might look something like the example below. You could invoke the code on a periodic basis using the Windows Task Scheduler.
# 1. If process is not running, then exit immediately
if (-not (Get-Process -Name notepad)) { throw 'Process is not running!'; return; }
# 2. Register for events when Notepad disappears
# 2a. Declare the WMI event query
$WmiEventQuery = "select * from __InstanceDeletionEvent within 5 where TargetInstance ISA 'Win32_Process' and TargetInstance.Name = 'notepad.exe'";
# 2b. Declare the PowerShell ScriptBlock that will execute when event is matched
# In this case, it simply appends the value of the $event automatic variable to a
# new, global variable named NotepadEvent.
$Action = { $global:NotepadEvent += $event; };
# 2c. Register for WMI events
Register-WmiEvent -Namespace root\cimv2 -Query $WmiEventQuery -Action $Action -SourceIdentifier NotepadStopped;
# 3. Wait indefinitely, or until $global:NotepadEvent variable is NOT $null
while ($true -and -not $global:NotepadEvent) {
Start-Sleep -Seconds 600;
(Invoke-WebRequest -Method post 'Http://website.com').Content;
}