VSCode & Powershell: How to stop script and unregister object? - powershell

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#

Related

How do you automatically clean up all event subscriptions made in a PowerShell script when that script exits?

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
}

Keep PowerShell process alive without interrupting asynchronous event handlers

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.

Windows Task Scheduler Powershell Script complete and restart

I have created a script that uses a FileSystemWatcher and creates a job. When a file is added to a folder it
Renames the newly added file
Moves the file to a new destination
Begins a downstream workflow
Once it’s done it ends the Job.
What I want to do is use Windows Task Scheduler to check that when the job completes to immediately run again. This way if one person drops a folder, that another person and can do the same.
ALSO If there is another way to have this work I am happy to use it.
I have tried to schedule the task to run but the lowest increment is 1 minute
I expect to have the filesystemwatcher running all the time. And once the script has finished I want it to start again.
You don't have to start job when the event (file creation) occur, you can directly move the file and do whatever you want using action parameter.
Also, you don't have to restart the event, it is will keep running unless you manually unregister it, so you don't have to use task scheduler at all.
Here's the code
# set the folder you want to watch
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "$home/Desktop/Folder"
$watcher.Filter = "*.*"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
# start job when event is detected
$action = {
Start-Job -ScriptBlock {
# Step 1&2 (You can move the file and rename it with one step)
Move-Item $Event.SourceEventArgs.FullPath -Destination $newPath
} | Wait-Job
}
# Register and start the FileSystemWatcher
Register-ObjectEvent -InputObject $watcher -EventName Created -SourceIdentifier FileCreated -Action $action
Note
I used Start-Job in the code; However, you can omit it and put the code exactly in $action.
This code will register new FileSystemWatcher and will keep monitoring the folder for newly created file then move it to a new location.
To stop the watcher
Unregister-Event -SourceIdentifier FileCreated
Look here for more information about tracking changes to a folder using PowerShell

DotNetZip ExtractProgress events in PowerShell

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)

Automatically pulling data from a PowerShell job while running

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.