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.
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#
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.
I want to catch a PowerShell script as it finishes so that I can do some final processing before it stops. Register-EngineEvent -SourceIdentifier PowerShell.Exiting seems to be the way to do it but it does not work for anything other than trivial applications.
Simple examples work. For example:
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {write-host "This is trivial"}
will print "This is trivial" when the script finishes. However, any action block that needs data passed to it does not get that data. For example:
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {write-host ">> " $($event.MessageData)} -MessageData "This doesn't work"
will only print the two leading angle brackets and not the string "This doesn't work".
Note that I'm calling the script from a Command Prompt (cmd.exe) and it is there that the Write-Host ultimately prints, after PowerShell has exited.
Furthermore, with the exception of the $Event automatic variables, all the others such as $Sender, $Args, $EventArgs etc. are not populated. Also some of the properties of $Event are not populated. For example, $Event.ComputerName prints nothing, but $Event.TimeGenerated prints the current date and time. My computer has a name.
I have included a tiny example program which demonstrates either that I am doing it wrong or that there is some limitation in what can be done with Register-EngineEvent maybe it is even a bug I suppose.
I have spent quite a lot of time searching web sites but I haven't found any examples where they are passing data to an action block for Register-EngineEvent.
sleep 1
write-host "Starting"
sleep 1
write-host "Finished"
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -MessageData "This doesn't work" -Action {write-host ">> " $($event.MessageData)}
sleep 2
I would expect that little script to print "Starting" followed 1 second later by "Finished" and then, after a further 2 seconds the event handler to print
">> This doesn't work". I do get the first two messages but all I get from the handler is ">>".
I am running on Windows 10 with Powershell V5. I am not running it in ISE. I use the command line powershell -file .\try6.ps1 where try6.ps1 is the script I've included above.
If anyone can suggest what I am doing wrong or alternative ways of doing this that would be great but even if it is just that it is a known bug or that I have misunderstood what PowerShell.exiting or Register-EngineEvent are and they can't be used in the way I am trying that would be very helpful as well.
Note: While this answer contains hopefully useful background information, it does not solve the OP's problem of wanting to pass custom event data to a PowerShell.Exiting event handler.
What Colin encountered is apparently a known bug: see GitHub issue #7000.
Generally, just to clarify: the PowerShell.Exiting event only fires on exiting a PowerShell session, not a script run inside a session.
Using it in the running session itself (as opposed to using it in a remote session that forwards the event to the caller) limits you to:
taking behind-the-scenes action when the session ends
using Write-Host to write output that the calling process potentially sees (use of implicit output or Write-Output is no longer an option, because PowerShell's output streams are no longer available at the time the event fires).
You're running a script locally via PowerShell's CLI, powershell.exe which means that the limitations above apply to you.
The automatic event-related variables documented in about_Automatic_Variables do not seem to contain much information when the event fires, as the following command demonstrates:
PS> powershell -c '$null =
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
$Event, $EventSubscriber, $Sender, $EventArgs, $Args | Out-string | Write-Host
}'
ComputerName :
RunspaceId : 1a763430-5cd8-4b74-aaaf-7a90e514518d
EventIdentifier : 1
Sender :
SourceEventArgs :
SourceArgs : {}
SourceIdentifier : PowerShell.Exiting
TimeGenerated : 3/26/19 12:15:48 AM
MessageData :
SubscriptionId : 1
SourceObject :
EventName :
SourceIdentifier : PowerShell.Exiting
Action : System.Management.Automation.PSEventJob
HandlerDelegate :
SupportEvent : False
ForwardEvent : False
$Event exists, and provides the runspace ID and a time stamp reflecting the time of the event.
$Event.ComputerName is presumably only populated if the event was forwarded (via Register-EngineEvent's -Forward switch) from a different computer in the context of remoting; if the property is empty, the implication is that the event fired on the local machine.
$EventSubscriber exists, but doesn't contain any useable information.
$EventArgs, $Sender and $Args are not populated.
Given the above, you could streamline your output to only contain the time stamp of the event and the local computer name's name:
PS> powershell -c '
$null =
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
[pscustomobject] #{ TimeGenerated = $Event.TimeGenerated; ComputerName = $env:COMPUTERNAME }| Format-List | Out-String | Write-Host
}
# ... call your script
.\try6.ps1
'
TimeGenerated : 3/26/2019 8:57:53 AM
ComputerName : Workstation10
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;
}