Why "Write-Host" works differently from PowerShell function and timer handler? - powershell

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

Related

PowerShell - how to stop script processing elapsed events from Timer class

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
}

Timer event in powershell not working in remote session

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

Powershell: Updating GUI from background job results

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.

Event gets triggered via ISE but not in normal PS window

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

Powershell Register-Object

I have a problem in running the below Powershell script in console.
function startMonitor {
$null = Register-ObjectEvent -InputObject ([Microsoft.Win32.SystemEvents]) -EventName "SessionSwitch" -Action {
switch($event.SourceEventArgs.Reason) {
'SessionLock'
{
---------do something----
}
'SessionUnlock'
{
--------do something----
}
}
}
}
startMonitor
When I run this in powershell ISE it works fine and output is as expected. When the session is locked or unlocked, output is generated perfectly.
But I want to run this as a script that starts up during logon.
I put this script in the startup folder as
powershell.exe -noexit -windowstyle hidden "E:\sources\lock.ps1"
The script runs fine. But, it does not generate the output (the other functions in this code generates output properly, except this function). When I try the command (without the switch -windowstyle):
Get-EventSubscriber
Shows that event is registered.
How do I run this script using the powershell.exe -noexit?
I am unable to use task scheduler for this purpose because of limitations in my environment.
In order to run in background you have to keep it within a loop and check for events, test with this little example...
[System.Reflection.Assembly]::LoadWithPartialName("System.Diagnostics")
# ------ init ------
function Start-mySession {
$null = Register-ObjectEvent -InputObject ([Microsoft.Win32.SystemEvents]) -EventName "SessionSwitch" -Action {
add-Type -AssemblyName System.Speech
$synthesizer = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer
switch($event.SourceEventArgs.Reason) {
'SessionLock' { $synthesizer.Speak("Lock!") }
'SessionUnlock' { $synthesizer.Speak("Unlock!") }
}
}
}
# ------ stop ------
function End-mySession {
$events = Get-EventSubscriber | Where-Object { $_.SourceObject -eq [Microsoft.Win32.SystemEvents] }
$jobs = $events | Select-Object -ExpandProperty Action
$events | Unregister-Event
$jobs | Remove-Job
}
# ===== keep process in alive for 10 hours =====
[TimeSpan]$GLOBAL:timeout = new-timespan -Hours 10
$GLOBAL:sw = [diagnostics.stopwatch]::StartNew()
while ($GLOBAL:sw.elapsed -lt $GLOBAL:timeout){
[Array]$GLOBAL:XX_DEBUG_INFO += "DEBUG: initialize debug!"
# Loop
Start-mySession
start-sleep -seconds 10
#.. do something, or check results
End-mySession
}
# teardown after timeout
Write-Output "INFO: $( get-date -format "yyyy-MM-dd:HH:mm:ss" ) : $env:USERNAME : Timeout (logout/timout)"
End-mySession