Summary
I'd like to know the best way to keep a PowerShell script running, without putting the main thread to sleep. Using Start-Sleep to keep a script alive has the side effect of blocking asycnhronous event handlers that are running on the same thread.
Example Code
Consider the following script, which demonstrates this blocking in action.
$Timer = [System.Timers.Timer]::new(500)
Register-ObjectEvent -InputObject $Timer -EventName Elapsed -Action { Write-Host -ForegroundColor Blue hi }
$Timer.Start()
Start-Sleep -Seconds 5
When running this script, you'll see that the Timer is blocked from executing its handlers until the Start-Sleep command has completed. However, you'll also notice that the events are queued up in the background, and all fire in quick succession once the main thread is released.
Also, the script exits once the Start-Sleep command has finished running.
Question: How do I keep the PowerShell process / script running, without blocking event handlers?
I asked a similar question on the PowerShell Polaris repository on GitHub about a year ago. The answer for that specific module is to use the PowerShell Wait-Event command.
Using the Wait-Event command, with a random -SourceIdentifier will block further execution of the main thread, without blocking event handlers from executing.
$Timer = [System.Timers.Timer]::new(500)
Register-ObjectEvent -InputObject $Timer -EventName Elapsed -Action { Write-Host -ForegroundColor Blue hi }
$Timer.Start()
Wait-Event -SourceIdentifier SomeEventIdThatWillNeverExist
Conditional Termination
If the script has "completed" (whatever that means to the author), and the registered event handlers wish to terminate the script, they can do so.
The -Action event handler on the Timer, in this example, could optionally fire an event with the SomeEventIdThatWillNeverExist as the value for its -SourceIdentifier parameter, using the built-in New-Event command. Otherwise, an outside could forcefully terminate the process when appropriate.
$Timer = [System.Timers.Timer]::new(500)
Register-ObjectEvent -InputObject $Timer -EventName Elapsed -Action {
Write-Host -ForegroundColor Blue hi
if ((Get-Random -Maximum 100) -gt 95) {
New-Event -SourceIdentifier SomeEventIdThatWillNeverExist
}
}
$Timer.Start()
Wait-Event -SourceIdentifier SomeEventIdThatWillNeverExist
🚨 In an interactive PowerShell session, the Timer will continue to run after the script has completed execution. You would need to invoke the script using a separate PowerShell process (ie. pwsh -File timer.ps1).
At the moment, this is the best answer that I am aware of. However, if there's a better way of accomplishing this, I'm open to new answers.
Related
I'm setting up a FileSystemWatcher to watch a file for changes. This works. I also want to keep the script that sets this up running until the user manually terminates the script. This also works. However, I'd also like the event subscription on the FileSystemWatcher to get automatically cleaned up when the script exits (either normally or abnormally). This doesn't work, because event subscriptions are part of the session, and the script doesn't have its own session.
I tried creating a new session object inside the script and using it for the watcher setup and event registration, which seemed to do a great job cleaning up the event subscription on script termination, but it also seemed to cause all my console activity to get swallowed up in that child session.
How can I make it so that whenever the script exits (normally or abnormally), the event subscription is cleaned up automatically? (And doing this while maintaining visibility of my console output.)
In case the context matters, this is a simple ZIP file build script. I'm trying to add a "watch mode" to it so that when the ZIP is updated by another app, the ZIP is decompressed back to the folder from which it was created. So this script is meant to be executed from a PowerShell command line that remains active and is possibly used for other things before and after this script runs. In other words, the mighty hammer of Get-EventSubscriber | Unregister-Event is potentially a little too mighty, in addition to being another command that the script user would have to invoke on their own.
This is a condensed version of my script:
$watcher = New-Object System.IO.FileSystemWatcher ".", $fileName -Property #{
NotifyFilter = [IO.NotifyFilters]::LastWrite
}
Register-ObjectEvent $watcher -EventName Changed -Action {
Write-Host "File change detected."
# other things, scripts, etc
}
$watcher.EnableRaisingEvents = $true
Write-Host "Press Ctrl+C to stop watching the file."
while ($true)
{
if ([Console]::KeyAvailable)
{
$keyInfo = [Console]::ReadKey($true)
if ($keyInfo.Modifiers -eq [ConsoleModifiers]::Control -and $keyInfo.Key -eq [ConsoleKey]::C)
{
Exit
}
}
else
{
Start-Sleep 0.5
}
}
Try the following:
$watcher = New-Object System.IO.FileSystemWatcher $pwd, $fileName -Property #{
NotifyFilter = [IO.NotifyFilters]::LastWrite
}
# Register with a self-chosen source identifier.
$evtJob = Register-ObjectEvent -SourceIdentifier fileWatcher $watcher -EventName Changed -Action {
Write-Host "File change detected."
# other things, scripts, etc
}
$watcher.EnableRaisingEvents = $true
Write-Host "Press Ctrl+C to stop watching the file."
try {
# This blocks execution indefinitely while allowing
# events to be processed in the -Action script block.
# Ctrl-C aborts the script by default, which will execute
# the `finally` block.
Wait-Event -SourceIdentifier fileWatcher
}
finally {
# Clean up the event job, and with it the event subscription.
# Note: If the -Action script block produces output, you
# can collect it with Receive-Job first.
$evtJob | Remove-Job -Force
}
I have a snippet that works fine. It registers an objectevent as follows (snippet):
Register-ObjectEvent $Watcher -EventName Created -SourceIdentifier FileCreated -Action {
It works fine, but I can't seem to get the script to fully stop and unregister. When I use "normal" methods to stop code (listed below), when I rerun the code (via F5) I get error:
Register-ObjectEvent : Cannot subscribe to the specified event. A subscriber with the source identifier 'FileCreated' already exists.
The only way I can unregister it (so far) is to terminate the powershell.exe process, which also kills the powershell run time within VSCode (throwing errors, forcing a manual restart).
I start the code by hitting F5
To stop the code, I have tried:
Shift-F5
Ctrl-C (in the powershell terminal area)
Nothing works except as described above.
What am I missing?
For debugging purposes only, where you often force-stop your script, there are 2 options. Either run the unregister-Event line manually so the event is unregistered or do add something like that near the top f your script. (at least before the Register-ObjectEvent )
Unregister-Event -SourceIdentifier FileCreated -EA 0
-EA 0 is the same as -ErrorAction SilentlyContinue and will prevent the script from throwing an error when you are on a new session and the event was never registered yet.
Note: This should not be in your production script and is only meant as a debugging help. (If you want to keep it on hand, I'd say comment it out at least on the prod. version of the script)
Your production version of the script should implement Unregister-Event properly though, something more akin to:
$FileWatcherReg = #{
InputObject = $Watcher
EventName = 'Created'
SourceIdentifier = 'FileCreated'
MessageData = #{WatchQueue = $WatchQueue; Timer = $WatchTimer }
Action = {
if ($null -ne $event) {
Write-Host "Path: $($Event.SourceArgs.FullPath) - ($($Event.Timegenerated))"
}
}
}
Register-ObjectEvent #FileWatcherReg
while ($true) {
Start-Sleep -Milliseconds 100
# Logic to exit the loop at some point.
}
# Exit script logic ...
Unregister-Event -SourceIdentifier FileCreated
Similar to Sage's example above, is this larger example. It structures the event clearly, and shows how by keeping the script running, you can then cancel the script, which will trigger the cleanup block.
So there is something to be said for this approach. (However, I continue to see the whole Register-ObjectEvent as kind of a TSR... your code stays resident... forever... waiting for the relevant event....)
The example:
https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/using-filesystemwatcher-correctly-part-2#
I am running in debug mode in ISE. I am starting a separate powershell script from another secondary script using start-process and specifying it's -file parameter from the primary script. I have a timer in the primary script where I am trying to execute some other code.
Please consider the following:
$timer = new-object timers.timer
$timerID = [guid]::NewGuid()
$timer.Interval = 10000
$complete = 0
$action = {
write-host "Complete Ticker: $complete"
if($complete -eq 1)
{
write-host "Completed"
$timer.stop()
Unregister-Event $timerID
}
}
Register-ObjectEvent -InputObject $timer -EventName elapsed –SourceIdentifier $timerID -Action $action
$timer.start()
$shellScript = "-file C:\Test.ps1"
$powershellArguments = $shellScript
Start-Process "powershell.exe" -Wait -ArgumentList $powershellArguments
$complete = 1
The other script is called successfully, and displays, however the timer does not appear to tick unless the script window started with the start-process is closed. The expected result is in that the interval specified that a breakpoint could be set in the action and hit while the secondary script is running.
Is there something perhaps wrong with the above code?
The start-process that starts PS uses the -Wait parameter. That prevents $action from running.
I'm not sure what the code is trying to accomplish via the timer and $action. What it would do, though (if not for -Wait), is write "Complete Ticker: 0" every ten seconds until the invoked PS completes when it will write "Complete Ticker: 1".
If the invoked PS runs longer than 10 seconds * height_of_the_parent_PS_console_in_lines, you won't really be able to see much in the way of output - the screen will be full of "Complete Ticker: 0" messages and you'd have to watch closely to see the screen kind of flicker every 10 seconds. Even if the invoked PS runs more quickly than that, the output doesn't tell how long the invoked PS will run or even that it's making progress.
In essence it's kind of like having the user watch der blinkenlights - interesting to look at possibly, but not too informative.
But if that's what the timer code is supposed to do it can be replaced by a simple loop after the start-process that checks if the invoked PS has completed and if not outputs the "Complete Ticker: 0" message.
Use the -passthru argument to start-process and then check the status of via the returned process object in the loop.
I'm extracting a ZIP file in PowerShell by using the DotNetZip library, and I want to display progress. I'm using the following code:
try {
$zip = [Ionic.Zip.ZipFile]::Read($ZipFileName)
Register-ObjectEvent `
-InputObject $zip `
-EventName ExtractProgress `
-SourceIdentifier ExtractProgress `
-Action {
[Console]::Beep()
Write-Host $Sender
Write-Host $SourceEventArgs
} | Out-Null
$zip.ExtractAll($Destination, 'OverwriteSilently')
}
finally {
Unregister-Event -SourceIdentifier ExtractProgress
$zip.Dispose()
}
My problem is that I don't see the events (no beep, no Write-Host) until the very end. I'm expecting to see progress events during the process.
Initially, I thought it was because Register-ObjectEvent queued the events, but the PowerShell help says that -Action is invoked immediately, without the event being queued.
If I write the equivalent code in a C# console application, then I see the progress events as each file is extracted, as expected, which means that (as far as I can tell) DotNetZip is doing the right thing. Note that the events are raised on the same thread that called ExtractAll.
What am I doing wrong?
(Windows 7 x64, PowerShell 2.0, configured to use .NET 4.0)
While trying to do something quite possibly beyond the means of PowerShell I seem to have ran into a brick wall. I have a main form script, which orchestrates most of my functions but I need another script to open a listener (system.Net.Sockets.Udpclient.Receive) and keep feeding in information to a textbox in the main form throughout the entire program's running.
For the life of me I can't get around this daft non-child environment that jobs suffer from; no dot sourcing, no global scoped variables, nothing. I can put an object-listener on it for statechanged to completion and then open another listener and try and bodge this way but it will get very messy and unreliable.
As a workaround I would love a TCP/UDP listener which doesn't hang the application for a response, an event to pull on hasmoredata or a way of updatign the textbox in the main script from within the job.
You can return data from a job by raising an event and forwarding it back to the local session.
Here is an example:
$job = Start-Job -Name "ReturnMessage" -ScriptBlock {
# forward events named "MyNewMessage" back to job owner
# this even works across machines
Register-EngineEvent -SourceIdentifier MyNewMessage -Forward
while($true) {
sleep 2
$i++
$message = "This is message $i."
# raise a new progress event, assigning to $null to prevent
# it ending up in the job's output stream
$null = New-Event -SourceIdentifier MyNewMessage -MessageData $message
}
}
$event = Register-EngineEvent -SourceIdentifier MyNewMessage -Action {
Write-Host $event.MessageData -ForegroundColor Green
}
<# Run this to stop job and event listner
$job,$event| Stop-Job -PassThru| Remove-Job
#>
Note that you can still type at the prompt while the job is running. Execute the code in the block comments to stop the job and event listner.