Using the Register-EnginerEvent -Forward and New-Event I am trying to forward object events from a remote server to the host server however it does not seem to work.
To prove the theory, tried the below simple code which does work:
$TargetServer = 'localhost'
Register-EngineEvent -SourceIdentifier TimerEventOccured -Action {
Write-Host "$(Get-Date -format "dd-MM-yyyy hh:mm:ss tt") - $($event.MessageData) received..." -ForegroundColor Green
} | Out-Null
$TimerScriptBlock = {
Register-EngineEvent -SourceIdentifier TimerEventOccured -Forward | Out-Null
$Count = 1
while($Count -lt 3) {
New-Event -SourceIdentifier TimerEventOccured -MessageData 'Timertriggered'
Start-Sleep -Seconds 5
$Count += 1
}
}
$RemoteTimerScriptBlockJob = Invoke-Command -ComputerName $TargetServer -ScriptBlock $TimerScriptBlock -AsJob
while($RemoteTimerScriptBlockJob.State -in #('NotStarted','Running')) {
Write-Host "$(Get-Date -format "dd-MM-yyyy hh:mm:ss tt") - remote timer job still running"
Start-Sleep -Seconds 5
}
Write-Host "$(Get-Date -format "dd-MM-yyyy hh:mm:ss tt") - remote timer job complete"
...where as the below adding Register-ObjectEvent, which is what I want to achieve, doesn't.
$TargetServer = 'localhost'
Register-EngineEvent -SourceIdentifier TimerEventOccured -Action {
Write-Host "$(Get-Date -format "dd-MM-yyyy hh:mm:ss tt") - $($event.MessageData) received..." -ForegroundColor Green
} | Out-Null
$TimerScriptBlock = {
Register-EngineEvent -SourceIdentifier TimerEventOccured -Forward | Out-Null
$timer = New-Object timers.timer
$timer.Enabled = $true
$timer.Interval = 3000
Register-ObjectEvent -InputObject $timer -EventName elapsed –SourceIdentifier thetimer -Action $action {
New-Event -SourceIdentifier TimerEventOccured -MessageData 'Timertriggered'
}
$timer.start()
Start-Sleep -Seconds 15 #just wait long enough for timer events to trigger a few times
}
$RemoteTimerScriptBlockJob = Invoke-Command -ComputerName $TargetServer -ScriptBlock $TimerScriptBlock -AsJob
while($RemoteTimerScriptBlockJob.State -in #('NotStarted','Running')) {
Write-Host "$(Get-Date -format "dd-MM-yyyy hh:mm:ss tt") - remote timer job still running"
Start-Sleep -Seconds 5
}
Write-Host "$(Get-Date -format "dd-MM-yyyy hh:mm:ss tt") - remote timer job complete"
Could you please help? Thanks.
Update:
Please note, I could directly forward the timer-event to the source server without needing the engine-event as the intermediary. But above timer event was only used to illustrate the point here. The real work I am dealing with is to monitor Windows Event log for certain event ids (which has become quite complex to share here).
So, if I were to use -forward directly on the Eventlog listener Object then it will create a lot of traffic from target servers to host session (i.e. every event written will be dispatched as opposed to the only ones I am after). I want to be able to process the triggered event first on the remote server itself (to match the input eventIDs) and then forward the filtered event through engine event, which is where I am stuck.
In short: Register-ObjectEvent isn't the problem in your case - it is the fact that you use a single Start-Sleep call after which you exit immediately, which causes most of the events to be lost.
When you suspend PowerShell's own foreground thread with Start-Sleep, you also suspend its event processing.
Specifically, this plays out as follows in your case:
While Start-Sleep runs, events are queued - the immediate side effect of which is that your events aren't processed in a timely fashion.
When Start-Sleep ends and the foreground thread regains control, the event queue starts getting processed, but since the script block ends right away, only an - unpredictable - subset of the queued events gets processed before overall execution of the remote script block ends. Seemingly, PowerShell doesn't ensure that queued events are processed before exiting.
Thus, if you break you single Start-Sleep -Seconds 15 call into multiple ones, giving PowerShell time to process events in between, your code should work:
1..3 | ForEach-Object { Start-Sleep -Seconds 5 }
Again, note that there's no guarantee that if events still happen to be queued afterwards that they will be processed before exiting.
However - as you've later discovered - you can use Wait-Event -Timeout as a superior alternative to Start-Process, as it does not block -Action script-block and -Forward event processing while it waits, allowing for the forwarded events to be processed in near-realtime.
Note: Wait-Event's (and also Get-Event's) primary purpose is to retrieve and output queued events, i.e. events that are not consumed by Register-ObjectEvent / Register-EngineEvent event subscriptions based on -Action or -Forward and must be retrieved and acted on on demand. However, as a beneficial side effect, Wait-Event also enables registration-based (subscriber-based) event processing (via -Action script blocks and -Forward) to occur while it waits.
The following self-contained example, which builds on your code:
Shows the use of Wait-Event, both in the remote script block and locally.
Retrieves the output produced directly by the remote script block, using Receive-Job
Performs cleanup, both of the remote job and the locale event subscription.
For details, refer to the source-code comments.
Note: Because "loopback remoting" is used, the local machine must be set up for remoting and you must run WITH ELEVATION (as admin) - the #Requires -RunAsAdministrator directive enforces the latter.
#Requires -RunAsAdministrator
# Running ELEVATED is a must if you use Invoke-Command -ComputerName with the local machine.
$TargetServer = 'localhost'
$eventJob = Register-EngineEvent -SourceIdentifier TimerEventOccurred -Action {
Write-Host "$(Get-Date -format "dd-MM-yyyy hh:mm:ss tt") - $($event.MessageData) received #$((++$i))..." -ForegroundColor Green
}
$TimerScriptBlock = {
$null = Register-EngineEvent -SourceIdentifier TimerEventOccurred -Forward
$timer = New-Object timers.timer
$timer.Interval = 1000 # Fire every second
$null = Register-ObjectEvent -InputObject $timer -EventName elapsed –SourceIdentifier thetimer -Action {
Write-Host "$(Get-Date -format "dd-MM-yyyy hh:mm:ss tt") - $($event.MessageData) TRIGGERED #$((++$i))..."
New-Event -SourceIdentifier TimerEventOccurred -MessageData 'Timertriggered'
}
$timer.start()
# Produce events for a certain number of seconds.
$secs = 5
# Wait-Event - unlike Start-Sleep - does NOT block the event processing.
# Note that since events created in this remote session are either forwarded
# or handled via an -Action script block, Wait-Event will produce *no output*.
Wait-Event -Timeout $secs
# Hack only to make this sample code work more predictably:
# Ensure that the last event gets processed too:
# -Timeout only accepts *whole* seconds and unpredictable runtime conditions
# can result in the last event to not have been processed yet when Wait-Event returns.
Start-Sleep -Milliseconds 100; Get-Event
"Exiting remote script block after $secs seconds."
}
$remoteTimerScriptBlockJob = Invoke-Command -ComputerName $TargetServer -ScriptBlock $TimerScriptBlock -AsJob
Write-Host "Processing events while waiting for the remote timer job to complete..."
do {
# Note that since the TimerEventOccurred is handled via an -Action script block,
# Wait-Event will produce *no output*, but it enables processing of those script blocks,
# unlike Start-Sleep.
Wait-Event -SourceIdentifier TimerEventOccurred -Timeout 3
} while ($remoteTimerScriptBlockJob.State -in 'NotStarted', 'Running')
Write-Host "$(Get-Date -format "dd-MM-yyyy hh:mm:ss tt") - Remote timer job terminated with the following output:"
# Receive the remote script block's output and clean up the job.
$remoteTimerScriptBlockJob | Receive-Job -Wait -AutoRemoveJob
# Also clean up the local event job.
$eventJob | Remove-Job -Force # -Force is needed, because event jobs run indefinitely.
# Note: This automatically also removes the job as an event subscriber, so there's no need
# for an additional Unregister-Event call.
Example output:
Related
It sounds like a reasonable expectation that events fired from one and the same thread should be received in the order in which they were fired. However, that doesn't seem to be the case. Is this a known/documented behavior, and is there any recourse to correct it?
Below are two ready-to-run code snippets that exhibit the issue, tested with PS v5.1 under both Win7 and Win10.
(a) Events fired from a thread in a separate job (i.e. a different process).
$events = 1000
$recvd = 0
$ooseq = 0
$job = Register-EngineEvent -SourceIdentifier 'Posted' -Action {
$global:recvd++
if($global:recvd -ne $event.messageData) {
$global:ooseq++
("-?- event #{0} received as #{1}" -f $event.messageData, $global:recvd)
} }
$run = Start-Job -ScriptBlock {
Register-EngineEvent -SourceIdentifier 'Posted' -Forward
for($n = 1; $n -le $using:events; $n++) {
[void] (New-Event -SourceIdentifier 'Posted' -MessageData $n)
} }
Receive-Job -Job $run -Wait -AutoRemoveJob
Unregister-Event -SourceIdentifier 'Posted'
Receive-Job -Job $job -Wait -AutoRemoveJob
if($events -eq $script:recvd) {
("total {0} events" -f $events)
} else {
("total {0} events events, {1} received" -f $events, $recvd)
}
if($ooseq -ne 0) {
("{0} out-of-sequence events" -f $ooseq)
}
Sample output from a failure case (out of a batch of 100 consecutive runs).
-?- event #545 received as #543
-?- event #543 received as #544
-?- event #546 received as #545
-?- event #544 received as #546
total 1000 events
4 out-of-sequence events
(b) Events fired from a separate runspace (i.e. a different thread).
$recvd = 0
$ooseq = 0
$job = Register-EngineEvent -SourceIdentifier 'Posted' -Action {
$global:recvd++
if($recvd -ne $event.messageData) {
$global:ooseq++
("-?- event #{0} received as #{1}" -f $event.messageData, $recvd)
}}
$sync = [hashTable]::Synchronized(#{})
$sync.Host = $host
$sync.events = 1000
$sync.posted = 0
$rs = [runspaceFactory]::CreateRunspace()
$rs.ApartmentState = "STA"
$rs.ThreadOptions = "ReUseThread"
$rs.Open()
$rs.SessionStateProxy.SetVariable("sync",$sync)
$ps = [powerShell]::Create().AddScript({
for($n = 1; $n -le $sync.events; $n++) {
$sync.Host.Runspace.Events.GenerateEvent('Posted', $null, $null, $n)
$sync.posted++
}})
$ps.runspace = $rs
$thd = $ps.BeginInvoke()
$ret = $ps.EndInvoke($thd)
$ps.Dispose()
Unregister-Event -SourceIdentifier 'Posted'
Receive-Job -Job $job -Wait -AutoRemoveJob
if($sync.events -eq $recvd) {
("total {0} events" -f $sync.events)
} else {
("total {0} events fired, {1} posted, {2} received" -f $sync.events, $sync.posted, $recvd)
}
if($ooseq -ne 0) {
("{0} out-of-sequence events" -f $ooseq)
}
Failure cases resemble the sample one posted under (a) above, except a few runs also had events dropped altogether. This, however, is more likely related to the other question Action-based object events sometimes lost.
total 1000 events fired, 1000 posted, 999 received
484 out-of-sequence events
[ EDIT ] I ran some additional tests for case (b) specifically, and confirmed that:
the receiving Action (where $global:recvd++) is always called on the same managed thread (this was confirmed by saving and comparing the [System.Threading.Thread]::CurrentThread.ManagedThreadId between calls);
the receiving Action is not re-entered during execution (this was confirmed by adding a global "nesting" counter, wrapping the Action between [System.Threading.Interlocked]::Increment/Decrement calls and checking that the counter never takes any values other than 0 and 1).
These eliminate a couple of possible race conditions, but still do not explain why the observed behavior is happening or how to correct it, so the original question remains open.
Is this a known/documented behavior?
"Normally" event handling is asynchronous by design. And this is the case in PowerShell with cmdlets like Register-EngineEvent -Action. This is indeed known and intended behaviour. You can read more about PowerShell eventing here and here. Both Microsoft sources point out, that this way of event handling is asynchronous:
PowerShell Eventing lets you respond to the asynchronous notifications
that many objects support.
and
NOTE These cmdlets can only be used for asynchronous .NET events. It’s
not possible to set up event handlers for synchronous events using the
PowerShell eventing cmdlets. This is because synchronous events all
execute on the same thread and the cmdlets expect (require) that the
events will happen on another thread. Without the second thread, the
PowerShell engine will simply block the main thread and nothing will
ever get executed.
So that's basically what you are doing. You forward events from your background job to the event subscriber that has an action defined and perform the action without blocking your background job. As far as I can tell, there is nothing more to expect. There is no requirement specified to process the forwarded events in any special order. Even the -Forward switch does not ensure anything more, except passing the events:
Indicates that the cmdlet sends events for this subscription to the
session on the local computer. Use this parameter when you are
registering for events on a remote computer or in a remote session.
It is hard and maybe impossible to find any documentation on the internals of the cmdlets. Keep in mind that Microsoft does not publish any documentation about internals afaik from the past, instead it is up to the MVPs to guess what happens inside and write books about it (drastically expressed).
So as there is no requirement to process the events in a certain order, and PowerShell just has the task to perfrom actions on an event queue, it is also allowed to perform those actions in parallel to accelerate the processing of the event queue.
Test your scripts on a VM with only one vCPU. The wrong order will still occur sometimes, but way more rarely. So less (real) parallelism, less possibilities to scramble the order. Of course, you cannot prevent the logical parallelism, implemented by different threads executed on one physical core. So some "errors" remain.
Is there any recourse to correct it?
I put "normally" into quotation marks, because there are ways to implement it synchronously. You will have to implement your own event handler of type System.EventHandler. I recommend reading this article to get an example for an implementation.
Another workaround is to store the events in an own event queue and sort them after collection (runs in ISE, not yet in PS):
$events = 10000
$recvd = 0
$ooseq = 0
$myEventQueue = #()
$job = Register-EngineEvent -SourceIdentifier 'Posted' -Action {$global:myEventQueue += $event}
$run = Start-Job -ScriptBlock {
Register-EngineEvent -SourceIdentifier 'Posted' -Forward
for($n = 1; $n -le $using:events; $n++) {
[void] (New-Event -SourceIdentifier 'Posted' -MessageData $n)
}
}
Receive-Job -Job $run -Wait -AutoRemoveJob
Unregister-Event -SourceIdentifier 'Posted'
Receive-Job -Job $job -Wait -AutoRemoveJob
Write-Host "Wrong event order in unsorted queue:"
$i = 1
foreach ($event in $myEventQueue) {
if ($i -ne $event.messageData) {
Write-Host "Event $($event.messageData) received as event $i"
}
$i++
}
$myEventQueue = $myEventQueue | Sort-Object -Property EventIdentifier
Write-Host "Wrong event order in sorted queue:"
$i = 1
foreach ($event in $myEventQueue) {
if ($i -ne $event.messageData) {
Write-Host "Event $($event.messageData) received as event $i"
}
$i++
}
Archived links:
PowerShell eventing async 1
PowerShell eventing async 2
PowerShell eventing sync
I am using Event related command in powershell, and when I register a object event and wait for it, my script will be paused by the wait-event call.
Based on the documentation,
The Wait-Event cmdlet suspends execution of a script or
function until a particular event is raised. Execution
resumes when the event is detected.
To cancel the wait, press CTRL+C.
But what I found is, when I press Ctrl-C in powershell console, the whole script is ended instead of wait-event call, which I thought the wait-event call maybe return with $null value.
I don't know if something wrong in my understanding, so hope anyone could share more idea on it.
$port = New-Object System.IO.Ports.SerialPort COM5,115200,None,8,One
$port.Open()
$subscription = Register-ObjectEvent -InputObject $port -EventName DataReceived -SourceIdentifier "DataReceived"
while($true)
{
$event = Wait-Event -SourceIdentifier "DataReceived"
#
# So how can I check if user press the Control-C to cancel wait-event
#
[System.IO.Ports.SerialPort]$sp = $event.Sender;
$line = $sp.ReadExisting();
Write-Host $event.EventIdentifier;
Write-Host $line;
Remove-Event -EventIdentifier $event.EventIdentifier;
}
Unregister-Event -SourceIdentifier "DataReceived"
$port.Close()
--EDIT---
Thanks for the answer, but I want to point out some known solution which I already tried.
[console]::TreatControlCAsInput = $true
if ([console]::KeyAvailable)
{
$key = [system.console]::readkey($true)
if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C"))
{
"Terminating..."
break
}
}
This is a good way to resolve the common Control-C scenario, but the problem is my application is paused by wait-event call, so I have no change to check the key input.
Especially, when I enable Ctrl C for console via [console]::TreatControlCAsInput = $true, Control-C will not be able to cancel Wait-Event any more, that is also a problem here.
Here is a good example, which you can try ( I rewrite a usable one for someone testing ):
$timer = New-Object System.Timers.Timer
[Console]::TreatControlCAsInput = $true;
$subscription = Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier "TimeElapsed"
$event = Wait-Event -SourceIdentifier "DataReceived"
Write-Host "User Cancelled"
Unregister-Event -SubscriptionId $subscription.Id
You can run this script, with and without [Console]::.... to test.
You might have to call [console]::TreatControlCAsInput = $true to tell that (Ctrl+C) is treated as ordinary input and use statements like those in your loop:
if ([console]::KeyAvailable)
{
$key = [system.console]::readkey($true)
if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C"))
{
"Terminating..."
break
}
}
Wait-Event isn't handling Ctrl+C, the PowerShell host (console host) handles Ctrl+C by stopping your script.
You can specify a timeout with Wait-Event and check for a key press after the time out. Try the following:
$timer = New-Object System.Timers.Timer
$subscription = Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier TimeElapsed
try
{
$oldTreatControlCAsInput = [Console]::TreatControlCAsInput
[Console]::TreatControlCAsInput = $true
Write-Host -NoNewline "Waiting for event "
do
{
$event = Wait-Event -SourceIdentifier DataReceived -Timeout 2
if ($null -eq $event -and [Console]::KeyAvailable)
{
$key = [Console]::ReadKey($true)
if ($key.KeyChar -eq 3)
{
Write-Host "Cancelled"
break
}
}
Write-Host -NoNewline "."
} while ($null -eq $event)
}
finally
{
[Console]::TreatControlCAsInput = $oldTreatControlCAsInput
Unregister-Event -SourceIdentifier TimeElapsed
}
The smallest timeout is 1 second so you may see a short lag between Ctrl+C and when the loop stops.
I am displaying a form that runs another script after a button is clicked. I need to check for the state of the script completion so that I can update the text of the button.
$ButtonEmailInfo.Add_Click({
$ButtonEmailInfo.Text = "Sending info"
$ButtonEmailInfo.Enabled = $false
$Form.Refresh()
Write-Host("Running start-job")
$global:SendClicked = $true
$global:SJob = Start-Job -filepath ($path + "\Send-Info.ps1")
$ButtonEmailInfo.Text = "Info sent"
$Form.Refresh()
})
The problem with the above code is that the button text is set to "Info sent" before the called script Send-Info.ps1 has completed.
I can test $global:SJob.Finished too see if the script has completed but I'm not sure how you do this in a form. Is there the equivalent of an update() method that gets checked periodically?
Thanks,
Rich
You can use Register-ObjectEvent to handle the job and perform an action when it has completed. You would add the following right after you begin the job.
Edit You will need to setup a Timer object to update the Window and allow the thread to handle the Event output from Register-ObjectEvent.
$global:SJob = Start-Job -filepath ($path + "\Send-Info.ps1")
Register-ObjectEvent -InputObject $Global:SJob -EventName StateChanged -SourceIdentifier JobWatcher -Action {
#Job completed actions go here
Write-Host "Job Completed!"
# $Event.Sender is the actual job object that you can either remove or retrieve data from
#Perform cleanup of event subscriber
Unregister-Event -SourceIdentifier $Event.SourceIdentifier
Remove-Job -Name $Event.SourceIdentifier -Force
}
Here is an example that you can run to see it in action without using it in a form that notifies when completed and then proceeds to remove the job and the event subscription.
$SJob = Start-Job {start-sleep -seconds 10} -Name TESTJOB
Register-ObjectEvent -InputObject $SJob -EventName StateChanged -SourceIdentifier JobWatcher -Action {
#Job completed actions go here
Write-Host "Job $($Event.Sender.Name) Completed!"
#Remove job
Remove-Job $Event.Sender
#Perform cleanup of event subscriber and its job
Unregister-Event -SourceIdentifier $Event.SourceIdentifier
Remove-Job -Name $Event.SourceIdentifier -Force
}
Edit You will need to setup a Timer object to update the Window and allow the thread to handle the Event output from Register-ObjectEvent. Add the following lines to your UI to create the timer as the window loads and a timer stop when it closes.
$Window.Add_Loaded({
##Configure a timer to refresh window##
#Create Timer object
$Script:timer = new-object System.Windows.Threading.DispatcherTimer
#Fire off every 5 seconds
$timer.Interval = [TimeSpan]"0:0:1.00"
#Add event per tick
$timer.Add_Tick({
[Windows.Input.InputEventHandler]{ $Script:Window.UpdateLayout() }
})
#Start timer
$timer.Start()
If (-NOT $timer.IsEnabled) {
$Window.Close()
}
})
$Window.Add_Closed({
$Script:timer.Stop()
})
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
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