Job to check job state - powershell

I'm trying to check what it's the state of the previously started jobs without blocking the session I am in.
So I tried something like this:
Start-Job -ScriptBlock {
while($true){
$i++
Start-Sleep -Seconds 1
}
}
Start-Job -ScriptBlock {
switch ((Get-Job)[0].State){
"Stopped" {
write-host "Stopped"
}
"Running"{
write-host "Running"
}
default {
write-host "Something else"
}
}
}
Get-Job | Receive-Job
But it throws an error saying that I can not index into a null array, even when I have several jobs running.
Is there a way for starting a job that allow me to check the other jobs state?

Since jobs "live" in different "spaces" (poor technicality, sorry for that), they can't see each other. So, the solution that I found is to run a ScriptBlock whenever a timer throws an event. This way, the other jobs continue undisturbed.
Start-Job -ScriptBlock {
while($true){
$i++
Start-Sleep -Seconds 1
}
}
$timer = New-Object System.Timers.Timer
$timer.AutoReset = $true #repeat?
$timer.Interval = 500 #ms
Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {
$job_state = (Get-Job)[0].State
switch ($job_state){
"Stopped" {
write-host "Stopped"
}
"Running"{
write-host "Running"
}
default {
write-host "Something else"
}
}
}
$timer.Start()
This way if the first job is in a state that isn't supposed to be, i can be notified.

Related

how do you detect if a job completes correctly with register-object event

I currently am sending off a job to be processed in the background, and im wondering if i give the client the option to cancel the background job, how do i get the state of the job in the background. for example down here I am using a register-object event state change to not slow down the software while its running, but i want to be able to tell if the job completed or was stopped. so I can send back an appropriate message to the user. I have been playing with this for a few days and haven't been able to wrap my mind around what to do.
$script:job = Start-Job -ScriptBlock {param($HostName, $username)
$Destination = "C:\Support\$($username)-$($hostname)-$(get-date -f "MM-dd-yyyy").zip"
[System.Windows.Forms.MessageBox]::Show("Report completed.", "Report")
} -ArgumentList ($HostName, $username)
$pso = new-object psobject -property #{job = $script:job.Id; JobState = $script:job.State} #statechanged
$jobEvent = Register-ObjectEvent $job Statechanged -MessageData $pso -Action {
if($event.messagedata.JobState -eq 'Complete')
{
[System.Windows.Forms.MessageBox]::Show("Report completed.", "Report")
}
else
{
[System.Windows.Forms.MessageBox]::Show("Report was canceled.", "Report")
}
}
Edit:

start-sleep will freeze Register-ObjectEvent action scriptblock

$EventHandlerScripBlock = {
if (! [String]::IsNullOrEmpty($EventArgs.Data)) {
$EventArgs.Data
}
}
$ProcessStdOutEvent = Register-ObjectEvent -InputObject $Process -Action $EventHandlerScripBlock -EventName 'OutputDataReceived'
$emptyStopWatch = [Diagnostics.Stopwatch]::StartNew()
while($true)
{
start-sleep -s 10
[array]$output = receive-job $ProcessStdOutEvent
if ($output)
{
$output
$emptyStopWatch.Restart()
}
elseif ($emptyStopWatch.elapsed.TotalMilliseconds -gt $OutputCheckIntervalInMs)
{
break;
}
}
I want to launch a process and read its standard output. I cannot directly read the standard output from the process by $process.standardoutput.ReadToEnd() because it will block and wait for the process exit.
so I need to use the async style like the upper code.
But I found that the start-sleep -s 10 will block the EventHandlerScripBlock execution. If I remove start-sleep -s 10, does it read the output too frequently?

How to limit while loop by time in powershell

I have a script which starts a process only after specific service is running.
It's a loop that's trying to Get-Service its status.
I can't find how to limit loop by time.
The part where I'm stuck:
#add Start button
$button_start = New-Object System.Windows.Forms.Button
$button_start.Location = New-Object System.Drawing.Size(25,70)
$button_start.Size = New-Object System.Drawing.Size(240,32)
$button_start.TextAlign = "MiddleCenter"
$button_start.font = New-Object System.Drawing.Font("Segoe UI",14,[System.Drawing.FontStyle]::Regular)
$button_start.BackColor = "seashell"
$button_start.Text = "Start"
$button_start.Add_Click({
#add statement
while ((Get-Service -ComputerName $textBox_IP.text -ServiceName wscsvc).Status -ne "Running") {
# Pause before next check
Start-Sleep -Seconds 1
}
#only then..
Start-Process -FilePath "C:\Users\username\Desktop\software.exe" -verb RunAs -ArgumentList $textBox_IP.text
})
$Form_remoteControl.Controls.Add($button_start)
I've tried internet searching information on network without any success.
Define a time limit and check if the current time exceeds that limit.
$limit = (Get-Date).AddMinutes(5)
while (... -or (Get-Date) -le $limit) {
Start-Sleep -Seconds 1
}
If you want to skip starting the external program when the service still isn't running after that add another check after the loop upon which you return:
if ((Get-Service ...).Status -ne "Running") {
return
}
This is an example how to stop a service and wait until it is stopped or timeout applies.
You can modify to start a service.
Function StopService ($serv)
{
Write-Host "Config service " $serv " ..."
$service = Get-Service $serv -ErrorAction SilentlyContinue
if ($service)
{
if($service.status -eq "running")
{
write-host "Stop service" $serv
Stop-Service $serv -Force
# Wait until service is stopped (max. 1 minute)
$acttime = 0
$waittime = 100
$maxtime = 60000
$TestService = Get-Service $serv
While($TestService | Where-Object {$_.Status -eq 'Running'})
{
Start-Sleep -m $waittime
$acttime += $waittime
if ($acttime -gt $maxtime)
{
write-host "ERROR: Service" $serv " could not be stopped!" -ForegroundColor Red
return $False
}
}
}
else
{
write-host "Service already stopped!" -ForegroundColor Green
return $True
}
}
else
{
write-host "Service not installed" -ForegroundColor Green
return $True
}
}
I recommend you not using any polling While loops (with Start-Sleep cmdlets) in a Windows forms interface. It will stall your interface for important form events as button clicks etc.
Instead, I would anticipate on the Windows.Forms Timer class by creating a timer event and take appropriate checks and actions after a certain time period (e.g. a new Start-Process depending on a service state).

Powershell Start-job synchronous output

I have a powershell script that starts a job
start-job -scriptblock {
while($true) {
echo "Running"
Start-Sleep 2
}
}
and then it continues executing the rest of the script.
That job, is kind of a monitoring one for the PID of that process.
I would like to synchronously print the PID every n seconds, without having to end the job.
For example, as the rest of the script is being executed, i would like to see output in my console.
Is something like that possible in powershell?
Thanks.
Yes, you can use events:
$job = Start-Job -ScriptBlock {
while($true) {
Register-EngineEvent -SourceIdentifier MyNewMessage -Forward
Start-Sleep -Seconds 3
$null = New-Event -SourceIdentifier MyNewMessage -MessageData "Pingback from job."
}
}
$event = Register-EngineEvent -SourceIdentifier MyNewMessage -Action {
Write-Host $event.MessageData;
}
for($i=0; $i -lt 10; $i++) {
Start-Sleep -Seconds 1
Write-Host "Pingback from main."
}
$job,$event| Stop-Job -PassThru| Remove-Job #stop the job and event listener
Credit goes to this answer. Other useful links:
How to Get Windows PowerShell to Notify You When a Job is Complete
Manage Event Subscriptions with PowerShell - Hey, Scripting Guy! Blog

How can you set a time limit for a PowerShell script to run for?

I want to set a time limit on a PowerShell (v2) script so it forcibly exits after that time limit has expired.
I see in PHP they have commands like set_time_limit and max_execution_time where you can limit how long the script and even a function can execute for.
With my script, a do/while loop that is looking at the time isn't appropriate as I am calling an external code library that can just hang for a long time.
I want to limit a block of code and only allow it to run for x seconds, after which I will terminate that code block and return a response to the user that the script timed out.
I have looked at background jobs but they operate in a different thread so won't have kill rights over the parent thread.
Has anyone dealt with this or have a solution?
Thanks!
Something like this should work too...
$job = Start-Job -Name "Job1" -ScriptBlock {Do {"Something"} Until ($False)}
Start-Sleep -s 10
Stop-Job $job
Here's my solution, inspired by this blog post. It will finish running when all has been executed, or time runs out (whichever happens first).
I place the stuff I want to execute during a limited time in a function:
function WhatIWannaDo($param1, $param2)
{
# Do something... that maybe takes some time?
Write-Output "Look at my nice params : $param1, $param2"
}
I have another funtion that will keep tabs on a timer and if everything has finished executing:
function Limit-JobWithTime($Job, $TimeInSeconds, $RetryInterval=5)
{
try
{
$timer = [Diagnostics.Stopwatch]::StartNew()
while (($timer.Elapsed.TotalSeconds -lt $TimeInSeconds) -and ('Running' -eq $job.JobStateInfo.State)) {
$totalSecs = [math]::Round($timer.Elapsed.TotalSeconds,0)
$tsString = $("{0:hh}:{0:mm}:{0:ss}" -f [timespan]::fromseconds($totalSecs))
Write-Progress "Still waiting for action $($Job.Name) to complete after [$tsString] ..."
Start-Sleep -Seconds ([math]::Min($RetryInterval, [System.Int32]($TimeInSeconds-$totalSecs)))
}
$timer.Stop()
$totalSecs = [math]::Round($timer.Elapsed.TotalSeconds,0)
$tsString = $("{0:hh}:{0:mm}:{0:ss}" -f [timespan]::fromseconds($totalSecs))
if ($timer.Elapsed.TotalSeconds -gt $TimeInSeconds -and ('Running' -eq $job.JobStateInfo.State)) {
Stop-Job $job
Write-Verbose "Action $($Job.Name) did not complete before timeout period of $tsString."
} else {
if('Failed' -eq $job.JobStateInfo.State){
$err = $job.ChildJobs[0].Error
$reason = $job.ChildJobs[0].JobStateInfo.Reason.Message
Write-Error "Job $($Job.Name) failed after with the following Error and Reason: $err, $reason"
}
else{
Write-Verbose "Action $($Job.Name) completed before timeout period. job ran: $tsString."
}
}
}
catch
{
Write-Error $_.Exception.Message
}
}
... and then finally I start my function WhatIWannaDo as a background job and pass it on to the Limit-JobWithTime (including example of how to get output from the Job):
#... maybe some stuff before?
$job = Start-Job -Name PrettyName -Scriptblock ${function:WhatIWannaDo} -argumentlist #("1st param", "2nd param")
Limit-JobWithTime $job -TimeInSeconds 60
Write-Verbose "Output from $($Job.Name): "
$output = (Receive-Job -Keep -Job $job)
$output | %{Write-Verbose "> $_"}
#... maybe some stuff after?
I know this is an old post, but I have used this in my scripts.
I am not sure if its the correct use of it, but the System.Timers.Timer that George put up gave me an idea and it seems to be working for me.
I use it for servers that sometimes hang on a WMI query, the timeout stops it getting stuck.
Instead of write-host I then output the message to a log file so I can see which servers are broken and fix them if needed.
I also don't use a guid I use the servers hostname.
I hope this makes sense and helps you.
$MyScript = {
Get-WmiObject -ComputerName MyComputer -Class win32_operatingsystem
}
$JobGUID = [system.Guid]::NewGuid()
$elapsedEventHandler = {
param ([System.Object]$sender, [System.Timers.ElapsedEventArgs]$e)
($sender -as [System.Timers.Timer]).Stop()
Unregister-Event -SourceIdentifier $JobGUID
Write-Host "Job $JobGUID removed by force as it exceeded timeout!"
Get-Job -Name $JobGUID | Remove-Job -Force
}
$timer = New-Object System.Timers.Timer -ArgumentList 3000 #just change the timeout here
Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action $elapsedEventHandler -SourceIdentifier $JobGUID
$timer.Start()
Start-Job -ScriptBlock $MyScript -Name $JobGUID
Here is an example of using a Timer. I haven't tried it personally, but I think it should work:
function Main
{
# do main logic here
}
function Stop-Script
{
Write-Host "Called Stop-Script."
[System.Management.Automation.Runspaces.Runspace]::DefaultRunspace.CloseAsync()
}
$elapsedEventHandler = {
param ([System.Object]$sender, [System.Timers.ElapsedEventArgs]$e)
Write-Host "Event handler invoked."
($sender -as [System.Timers.Timer]).Stop()
Unregister-Event -SourceIdentifier Timer.Elapsed
Stop-Script
}
$timer = New-Object System.Timers.Timer -ArgumentList 2000 # setup the timer to fire the elapsed event after 2 seconds
Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier Timer.Elapsed -Action $elapsedEventHandler
$timer.Start()
Main
How about something like this:
## SET YOUR TIME LIMIT
## IN THIS EXAMPLE 1 MINUTE, BUT YOU CAN ALSO USE HOURS/DAYS
# $TimeSpan = New-TimeSpan -Days 1 -Hours 2 -Minutes 30
$TimeSpan = New-TimeSpan -Minutes 1
$EndTime = (Get-Date).AddMinutes($TimeSpan.TotalMinutes).ToString("HH:mm")
## START TIMED LOOP
cls
do
{
## START YOUR SCRIPT
Write-Warning "Test-Job 1...2...3..."
Start-Sleep 3
Write-Warning "End Time = $EndTime`n"
}
until ($EndTime -eq (Get-Date -Format HH:mm))
## TIME REACHED AND END SCRIPT
Write-Host "End Time reached!" -ForegroundColor Green
When using hours or days as a timer, make sure you adjust the $TimeSpan.TotalMinutes
and the HH:mm format, since this does not facilitate the use of days in the example.
I came up with this script.
Start-Transcript to log all actions and save them to a file.
Store the current process ID value in the variable $p then write it to screen.
Assign the current date to the $startTime variable.
Afterwards I assign it again and add the extra time to the current date to the var $expiration.
The updateTime function return what time there is left before the application closes. And writes it to console.
Start looping and kill process if the timer exceeds the expiration time.
That's it.
Code:
Start-Transcript C:\Transcriptlog-Cleanup.txt #write log to this location
$p = Get-Process -Id $pid | select -Expand id # -Expand selcts the string from the object id out of the current proces.
Write-Host $p
$startTime = (Get-Date) # set start time
$startTime
$expiration = (Get-Date).AddSeconds(20) #program expires at this time
# you could change the expiration time by changing (Get-Date).AddSeconds(20) to (Get-Date).AddMinutes(10)or to hours whatever you like
#-----------------
#Timer update function setup
function UpdateTime
{
$LeftMinutes = ($expiration) - (Get-Date) | Select -Expand minutes # sets minutes left to left time
$LeftSeconds = ($expiration) - (Get-Date) | Select -Expand seconds # sets seconds left to left time
#Write time to console
Write-Host "------------------------------------------------------------------"
Write-Host "Timer started at : " $startTime
Write-Host "Current time : " (Get-Date)
Write-Host "Timer ends at : " $expiration
Write-Host "Time on expire timer : "$LeftMinutes "Minutes" $LeftSeconds "Seconds"
Write-Host "------------------------------------------------------------------"
}
#-----------------
do{ #start loop
Write-Host "Working"#start doing other script stuff
Start-Sleep -Milliseconds 5000 #add delay to reduce spam and processing power
UpdateTime #call upadate function to print time
}
until ($p.HasExited -or (Get-Date) -gt $expiration) #check exit time
Write-Host "done"
Stop-Transcript
if (-not $p.HasExited) { Stop-Process -ID $p -PassThru } # kill process after time expires