I'm writing a PowerShell script in Visual Studio Code which is intended to run a function for every tick of a timer. Here is the code:
$timer = New-Object Timers.Timer
$timer.Interval = 1000
$timer.Enabled = $true
Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {Write-Host "Tick"}
The problem is that I don't seem to be able to stop the script using Ctrl+C which is what I would normally do, or indeed anything other than actually killing the terminal using the 'dustbin' icon. It just continues printing 'Tick'!
What's going on here? How would I stop this script gracefully?
When you subscribe an action to an event using Register-ObjectEvent, it can be unregistered using the Unregister-Event cmdlet:
$timer = New-Object Timers.Timer
$timer.Interval = 1000
$timer.Enabled = $true
Register-ObjectEvent -SourceIdentifier MyElapsedTick -InputObject $timer -EventName Elapsed -Action {Write-Host "Tick"}
<# ... #>
Unregister-Event -SourceIdentifier MyElapsedTick
To enumerate existing subscribers (for when you forget to specify a SourceIdenfitier for example), use Get-EventSubscriber:
# This will unregister all event subscribers
Get-EventSubscriber |Unregister-Event
$timer = [System.Timers.Timer]::new()
$timer.Interval = 5000
$timer.AutoReset = $false
$timer.Enabled = $true
1..20 | % {
if (!$timer.Enabled){
exit
}
write-host $_, $timer.Enabled
Start-Sleep -Milliseconds 1000
}
Related
I have a remote PowerShell session from machine A to B. Requirement is to do a continuous check inside B. On a specific condition, I have to execute something and stop the timer. For that, I created a timer function. But that timer function is not starting.
The code that I have written is:
$timer = new-object timers.timer
$action = {
write-host "inside loop.."
# Condition to check comes here.
}
$timer.Enabled = $true
$timer.Interval = 3000 #3 seconds
Register-ObjectEvent -InputObject $timer -EventName elapsed –SourceIdentifier thetimer -Action $action
$timer.start()
I expect the output to be
inside loop..
inside loop..
inside loop..
but the actual output is:
Id----------------1
Name--------------thetimer
PSJobTypeName----------
State-------------NotStarted
HasMoreData-------False
EDIT : I was able to get it working, see below for my solution. The commenters below are correct that Powershell isn't really ideal for GUI's and threading, but it can be done.
I've got a form in Powershell that uses Start-Job to run functions in the background without freezing the GUI. My goal is to continuously check the status of those jobs for their output. I managed to use the Windows Forms Timer to check the results of the job and update the GUI accordingly.
It's all working fine, but it seems sloppy. Is this the best way to accomplish a GUI refresh? I'm relatively new to Powershell and I want to improve my coding.
Example of what I'm doing:
$jobScript = {
Start-Sleep 5
Write-Output "The job has finished running"
}
$timerScript = {
$timer.Stop()
$jobResult = Get-Job | Receive-Job -Keep
if ($jobResult) {
$btn.text = $jobResult
} else {
$timer.Start()
}
}
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.ClientSize = '300,300'
$form.topmost = $true
$btn = New-Object System.Windows.Forms.Button
$btn.Text = "The job is still running"
$btn.Width = 300
$btn.Height = 300
$form.Controls.Add($btn)
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 100
$timer.add_Tick($timerScript)
$timer.Start()
Start-Job -ScriptBlock $jobScript
$form.ShowDialog()
Update: My solution
Using Register-ObjectEvent did not work, it seemed like it was fighting with the GUI for the thread. Instead I was able to use [System.Windows.Forms.Application]::DoEvents(). This allows the GUI to be moved around, and once it's done being moved, the thread will resume. The big caveat here is that execution is paused as long as the GUI is being moved, so if your code needs to react to the background job on a time limit, this could cause errors.
Example code block:
$jobScript =
{
Start-Sleep 5
Write-Output "The job is completed"
}
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.ClientSize = '300,300'
$form.topmost = $true
$btn = New-Object System.Windows.Forms.Button
$btn.Text = "..."
$btn.Width = 300
$btn.Height = 300
$form.Controls.Add($btn)
$btn.add_click({
$btn.Text = "Starting job"
$jobby = Start-Job -ScriptBlock $jobScript
Do {[System.Windows.Forms.Application]::DoEvents()} Until ($jobby.State -eq "Completed")
$btn.Text = Get-Job | Receive-Job
})
$form.ShowDialog()
You can use events:
$job = Start-Job {sleep 3; Write-Output "Dummy job completed"}
$callback = {
Write-Host "Event Fired"
Unregister-Event -SourceIdentifier "DummyJob"
Write-Host ($job | Receive-Job)
}
$evt = Register-ObjectEvent -InputObject $job -EventName StateChanged -SourceIdentifier "DummyJob" -Action $callback
# to remove all
Get-Job | Remove-Job -Force # jobs and events
Get-EventSubscriber | Unregister-Event # events
You might want to post this on CodeReview.StackExchange.com.
I sort of hate it when people build UI's in Powershell. If you want a proper Windows forms app, just write it in C#. So I disagree with the design at its premise.
I like your impulse to move away from the polling design; you initiate the job then poll to see if it's completed. I think an event handler might be a better choice. Check out the section "Monitor a Background Job" in the article PowerShell and Events: Object Events. It's an oldie but a goodie.
I have 1809 Windows 10 box with PowerShell Core 6.1.1
Given following code sample:
function Test() {
Write-Host "Test"
}
function Invoke-Test() {
$timer = New-Object System.Timers.Timer
$timer.AutoReset = $false
$timer.Interval = 1
Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {
Test
}
$timer.Enabled = $true
}
If I invoke "Test" function, I get "Test" output as expected:
But if I schedule invocation with a timer, command prompt is completely messed up:
I vaguely understand that it's something related to internal "readline" and console mechanic, but is it any way to produce newline output followed by a command prompt from a timer/handle in powershell?
Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier Timer.Test -Action {
Test
}
$timer.Enabled = $true
PS C:\> $Subscriber = Get-EventSubscriber -SourceIdentifier Timer.Test
PS C:\> $Subscriber.action | Format-List -Property *
The command property should hold the results of your “test” function.
The url below has detailed explanations.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-eventsubscriber?view=powershell-6
I've tried a lot of "works in ise but not in console" links via Google and especially here on Stackoverflow, but alas nothing I've tried so far worked.
I'll start with the whole code:
Param (
[ValidateRange(2,10)][int]$Time = 5,
[ValidateLength(4,40)][String]$Title = "Attention $($Env:USERNAME.ToUpper())",
[ValidateLength(4,40)][String]$Text = "This is a test!",
[ValidateSet("None","Info","Warning","Error")] $Status = "Info",
[ValidatePattern("[\\:\-\w\d\s]+.exe")][String]$TrayIcon = (Get-Process -id $pid).Path
)
# sec to ms
$TTL = $Time * 1000
if (![System.IO.File]::Exists($TrayIcon)){
Write-Host "File does not exist!"
exit
}
function ClearEvents {
# Perform cleanup actions on balloon tip
$global:balloon.dispose()
Remove-Variable -Name balloon -Scope Global
Unregister-Event -SourceIdentifier TrayClicked
Remove-Job -Name TrayClicked
Unregister-Event -SourceIdentifier BallonClicked
Remove-Job -Name BallonClicked
Unregister-Event -SourceIdentifier BalloonClosed
Remove-Job -Name BalloonClosed
#test if function is called
[System.Windows.MessageBox]::Show('Hello')
}
Add-Type -AssemblyName System.Windows.Forms
$global:balloon = New-Object System.Windows.Forms.NotifyIcon
# Tray-Icon
$balloon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($TrayIcon)
# Balloon
# [System.Windows.Forms.ToolTipIcon] | Get-Member -Static -Type Property
$balloon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::$Status
$balloon.BalloonTipTitle = $Title
$balloon.BalloonTipText = $Text
$balloon.Visible = $true
$balloon.ShowBalloonTip($TTL)
[void](Register-ObjectEvent -InputObject $balloon -EventName MouseClick -SourceIdentifier TrayClicked -Action {
ClearEvents
})
[void](Register-ObjectEvent -InputObject $balloon -EventName BalloonTipClicked -SourceIdentifier BallonClicked -Action {
ClearEvents
})
[void](Register-ObjectEvent -InputObject $balloon -EventName BalloonTipClosed -SourceIdentifier BalloonClosed -Action {
ClearEvents
})
Now, when one of the events are triggered (like Balloon clicked), then the "ClearEvents" function gets called (for testing purposes I added a MsgBox).
But in a console these events never trigger! Why is that?
Tested with Win7x64 (PS 3.0) and Win10x64 (PS 5.1)
I've also confirmed with [System.Threading.Thread]::CurrentThread.GetApartmentState() that both consoles run indeed with the default "STA" (Single-Threaded Apartment), which apparently some "System.Windows.Forms" need to run correctly.
Unfortunately that's not the problem...
My script is based on this one: https://mcpmag.com/articles/2017/09/07/creating-a-balloon-tip-notification-using-powershell.aspx
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.