Strange function call behavior with respect to .NET events - powershell

The code below will correctly display a message box when the notification icon is clicked.
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$NotifyIcon = New-Object System.Windows.Forms.NotifyIcon
$NotifyIcon.Icon = New-Object System.Drawing.Icon(Join-Path $PSScriptRoot "icon.ico")
$NotifyIcon.Visible = $True
Register-ObjectEvent -InputObject $NotifyIcon -EventName Click -Action {
Write-Host "Callback called."
[System.Windows.Forms.MessageBox]::Show("Test")
}
(The program won't work without a correctly specified icon file. You can download one icon here, for example.)
If I move the line displaying the message box into a function, the code fails to display that message box:
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
function Do-Something {
Write-Host "Do-Something called."
[System.Windows.Forms.MessageBox]::Show("Test")
}
$NotifyIcon = New-Object System.Windows.Forms.NotifyIcon
$NotifyIcon.Icon = New-Object System.Drawing.Icon(Join-Path $PSScriptRoot "icon.ico")
$NotifyIcon.Visible = $True
Register-ObjectEvent -InputObject $NotifyIcon -EventName Click -Action {
Write-Host "Callback called."
Do-Something
}
The first strange aspect is that the second code writes Callback called to the console - but only once. After one click, it will no longer print any debug messages.
I ran both scripts using powershell.exe and typing ./script[1|2].ps1. (I don't recommend running both scripts simultaneously since they use the same icon. Closing the PowerShell window terminates the script and deletes the icon resource. The icon stays present until you move your mouse over it, however.)
The second strange fact appears when running the scripts via the Windows PowerShell ISE: They both work like a charm.
Trying to set breakpoints in the two lines of the callback handler in the second script didn't work for me. PowerShell ISE always gives a warning when exactly those lines are executed:
PS P:\...> WARNUNG: Haltepunkt Zeilenhaltepunkt bei "P:\...\script.ps1:13" wird nicht erreicht.
Callback called.
WARNUNG: Haltepunkt Zeilenhaltepunkt bei "P:\...\script.ps1:14" wird nicht erreicht.
Do-Something called.
Translation from German into English:
PS P:\...> WARNING: Breakpoint line breakpoint at "P:\...\script.ps1:13" doesn't get reached.
Callback called.
WARNING: Breakpoint line breakpoint at "P:\...\script.ps1:14" doesn't get reached.
Do-Something called.
The same problem occurs when trying to debug the first script.
Searching for the issue in the Internet turned out to be rather hard. I tried powershell net callback called once (among others), but I couldn't find anything.

The Problem
The problem that Do-Something could not be called is due to scope issues, I suppose. Since Register-ObjectEvent returns an event job if an action is provided, I think the execution of the action block acts like the one of jobs, which run in a different PowerShell process.
Another supportive argument would be that calling Do-SomethingXYZ (which obvisouly doesn't exist) doesn't raise any (visible?) errors.
My Solution
Suggestions for more idiomatic solutions are welcome!
Use the MessageData parameter of Register-ObjectEvent to overcome the scope problem.
Use ${function:xyz} to pass a function as a parameter.
Invoke the function using $Event.MessageData.Invoke().
The code:
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
function Do-Something {
Write-Host "Do-Something called."
[System.Windows.Forms.MessageBox]::Show("Test")
}
$NotifyIcon = New-Object System.Windows.Forms.NotifyIcon
$NotifyIcon.Icon = New-Object System.Drawing.Icon(Join-Path $PSScriptRoot "icon.ico")
$NotifyIcon.Visible = $True
Register-ObjectEvent -MessageData ${function:Do-Something} -InputObject $NotifyIcon -EventName Click -Action {
Write-Host "Callback called."
$Event.MessageData.Invoke()
}
Further comments
The first strange aspect: Callback called only once?
The first strange aspect is that the second code writes Callback called to the console - but only once. After one click, it will no longer print any debug messages.
— ComFreek (in the question)
The code is as follows:
Write-Host "Callback called."
Do-Something
I admit, the debug message is a bit misleading. At the time of writing the message via Write-Host, Do-Something hasn't been called yet. Since PowerShell is not able to resolve Do-Something, it silently fails. That is probably fatal enough to stop calling the action handler, registered by Register-ObjectEvent, a second time. Therefore, the debug message only appears once.
The second strange aspect: Why do both scripts work with PowerShell ISE?
This question is not fully resolved, but I might just add the information I have observed so far.
The program acts like a REPL (Read-eval-print loop) equipped with many useful features. The point is that it only uses one PowerShell session.
You can observe that behavior by running a script containing $a = "Hello", clearing the file and then outputting the value by running $a. All objects (variables, functions, etc.) remain alive and share the same scope with other script executions.
Open questions / questions you might consider
Why does PowerShell not throw an error when an unknown function in an action handler is called?
Is there something like an error level which restricts certain kinds of errors from being shown?
Passing -ErrorAction stop to Register-ObjectEvent does not help.
Why do the breakpoints apparently not get reachted, but their associated line executed?
Answers or comments to these questions are more than welcome :)

Related

How to prevent input from displaying in console while script is running

I have a script that runs several loops of code and relies on specific input at various phases in order to advance. That functionality is working. My current issue revolves around extraneous input being supplied by the user displaying on screen in the console window wherever I have the cursor position currently aligned.
I have considered ignoring this issue since the functionality of the script is intact, however, I am striving for high standards with the console display of this script, and I would like to know a way to disable all user input period, unless prompted for. I imagine the answer has something to do with being able to command the Input Buffer to store 0 entries, or somehow disabling and then re-enabling the keyboard as needed.
I have tried using $HOST.UI.RawUI.Flushinputbuffer() at strategic locations in order to prevent characters from displaying, but I don't think there's anywhere I could put that in my loop that will perfectly block all input from displaying during code execution (it works great for making sure nothing gets passed when input is required, though). I've tried looking up the solution, but the only command I could find for manipulating the Input Buffer is the one above. I've also tried strategic implementation of the $host.UI.RawUI.KeyAvailable variable to detect keystrokes during execution, then $host.UI.RawUI.ReadKey() to determine if these keystrokes are unwanted and do nothing if they are, but the keystrokes still display in the console no matter what.
I am aware that this code is fairly broken as far as reading the key to escape the loop goes, but bear with me. I hashed up this example just so that you could see the issue I need help eliminating. If you hold down any letter key during this code's execution, you'll see unwanted input displaying.
$blinkPhase = 1
# Set Coordinates for cursor
$x = 106
$y = 16
$blinkTime = New-Object System.Diagnostics.Stopwatch
$blinkTime.Start()
$HOST.UI.RawUI.Flushinputbuffer()
do {
# A fancy blinking ellipses I use to indicate when Enter should be pressed to advance.
$HOST.UI.RawUI.Flushinputbuffer()
while ($host.UI.RawUI.KeyAvailable -eq $false) {
if ($blinkTime.Elapsed.Milliseconds -gt 400) {
if ($blinkPhase -eq 1) {
[console]::SetCursorPosition($x,$y)
write-host ". . ." -ForegroundColor gray
$blinkPhase = 2
$blinkTime.Restart()
} elseif ($blinkPhase -eq 2) {
[console]::SetCursorPosition($x,$y)
write-host " "
$blinkPhase = 1
$blinkTime.Restart()
}
}
start-sleep -m 10
}
# Reading for actual key to break the loop and advance the script.
$key = $host.UI.RawUI.ReadKey()
} while ($key.key -ne "Enter")
The expected result is that holding down any character key will NOT display the input in the console window while the ellipses is blinking. The actual result, sans error message, is that a limited amount of unwanted/unnecessary input IS displaying in the console window, making the script look messy and also interfering with the blinking process.
What you're looking for is to not echo (print) the keys being pressed, and that can be done with:
$key = $host.UI.RawUI.ReadKey('IncludeKeyDown, NoEcho')
Also, your test for when Enter was pressed is flawed[1]; use the following instead:
# ...
} while ($key.Character -ne "`r")
Caveat: As of at least PSReadLine version 2.0.0-beta4, a bug causes $host.UI.RawUI.KeyAvailable to report false positives, so your code may not work as intended - see this GitHub issue.
Workaround: Use [console]::KeyAvailable instead, which is arguably the better choice anyway, given that you're explicitly targeting a console (terminal) environment with your cursor-positioning command.
As an aside: You can simplify and improve the efficiency of your solution by using a thread job to perform the UI updates in a background thread, while only polling for keystrokes in the foreground:
Note: Requires the ThreadJob module, which comes standard with PowerShell Core, and on Windows PowerShell can be installed with Install-Module ThreadJob -Scope CurrentUser, for instance.
Write-Host 'Press Enter to stop waiting...'
# Start the background thread job that updates the UI every 400 msecs.
# NOTE: for simplicity, I'm using a simple "spinner" here.
$jb = Start-ThreadJob {
$i=0
while ($true) {
[Console]::Write("`r{0}" -f '/-\|'[($i++ % 4)])
Start-Sleep -ms 400
}
}
# Start another thread job to do work in the background.
# ...
# In the foreground, poll for keystrokes in shorter intervals, so as
# to be more responsive.
While (-not [console]::KeyAvailable -or ([Console]::ReadKey($true)).KeyChar -ne "`r" ) {
Start-Sleep -Milliseconds 50
}
$jb | Remove-Job -Force # Stop and remove the background UI thread.
Note the use of [Console]::Write() in the thread job, because Write-Host output wouldn't actually be passed straight through to the console.
[1] You tried to access a .Key property, which only the [SystemConsoleKeyInfo] type returned by [console]::ReadKey() has; the approximate equivalent in the $host.UI.rawUI.ReadKey() return type, [System.Management.Automation.Host.KeyInfo], is .VirtualKeyCode, but its specific type differs, so you can't (directly) compare it to "Enter"; The latter type's .Character returns the actual [char] instance pressed, which is the CR character ("`r") in the case of Enter.

how to prevent external script from terminating your script with break statement

I am calling an external .ps1 file which contains a break statement in certain error conditions. I would like to somehow catch this scenario, allow any externally printed messages to show as normal, and continue on with subsequent statements in my script. If the external script has a throw, this works fine using try/catch. Even with trap in my file, I cannot stop my script from terminating.
For answering this question, assume that the source code of the external .ps1 file (authored by someone else and pulled in at run time) cannot be changed.
Is what I want possible, or was the author of the script just not thinking about playing nice when called externally?
Edit: providing the following example.
In badscript.ps1:
if((Get-Date).DayOfWeek -ne "Yesterday"){
Write-Warning "Sorry, you can only run this script yesterday."
break
}
In myscript.ps1:
.\badscript.ps1
Write-Host "It is today."
The results I would like to achieve is to see the warning from badscript.ps1 and for it to continue on with my further statements in myscript.ps1. I understand why the break statement causes "It is today." to never be printed, however I wanted to find a way around it, as I am not the author of badscript.ps1.
Edit: Updating title from "powershell try/catch does not catch a break statement" to "how to prevent external script from terminating your script with break statement". The mention of try/catch was really more about one failed solution to the actual question which the new title better reflects.
Running a separate PowerShell process from within my script to invoke the external file has ended up being a solution good enough for my needs:
powershell -File .\badscript.ps1 will execute the contents of badscript.ps1 up until the break statement including any Write-Host or Write-Warning's and let my own script continue afterwards.
I get where you're coming from. Probably the easiest way would be to push the script off as a job, and wait for the results. You can even echo the results out with Receive-Job after it's done if you want.
So considering the bad script you have above, and this script file calling it:
$path = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$start = Start-Job -ScriptBlock { . "$using:Path\badScript.ps1" } -Name "BadScript"
$wait = Wait-Job -Name "BadScript" -Timeout 100
Receive-Job -Name "BadScript"
Get-Command -Name "Get-ChildItem"
This will execute the bad script in a job, wait for the results, echo the results, and then continue executing the script it's in.
This could be wrapped in a function for any scripts you might need to call (just to be on the safe side.
Here's the output:
WARNING: Sorry, you can only run this script yesterday.
CommandType Name Version Source
----------- ---- ------- ------
Cmdlet Get-ChildItem 3.1.0.0 Microsoft.PowerShell.Management
In the about_Break documentation it says
PowerShell does not limit how far labels can resume execution. The
label can even pass control across script and function call
boundaries.
This got me thinking, "How can I trick this stupid language design choice?". And the answer is to create a little switch block that will trap the break on the way out:
.\NaughtyBreak.ps1
Write-Host "NaughtyBreak about to break"
break
.\OuterScript.ps1
switch ('dummy') { default {.\NaughtyBreak.ps1}}
Write-Host "After switch() {NaughtyBreak}"
.\NaughtyBreak.ps1
Write-Host "After plain NaughtyBreak"
Then when we call OuterScript.ps1 we get
NaughtyBreak about to break
After switch() {NaughtyBreak}
NaughtyBreak about to break
Notice that OuterScript.ps1 correctly resumed after the call to NaughtyBreak.ps1 embedded in the switch, but was unceremoniously killed when calling NaughtyBreak.ps1 directly.
Putting break back inside a loop (including switch) where it belongs.
foreach($i in 1) { ./badscript.ps1 }
'done'
Or
switch(1) { 1 { ./badscript.ps1 } }
'done'

The PowerShell ISE sometimes behaves unpredictably after code changes are made

I'm using the PowerShell ISE (PS version 5.0). If I run this code:
Write-Host "This"
It outputs:
This
If I modify the script like this:
Write-Host "That"
It outputs:
That
Great. As expected. Now, if I have this code:
$Form = New-Object System.Windows.Forms.Form
$Timer = New-Object System.Windows.Forms.Timer
$Timer.Add_Tick(
{
&{
Write-Output "Here"
$Form.Close()} | Write-Host
})
$Timer.Interval = 3000
$Timer.start()
$result = $Form.ShowDialog()
It outputs:
Here
If I change anything in the script, e.g. "Here" to "There" or $Timer.Interval = 3000 to $Timer.Interval = 4000 and run it, it does two unexpected things: 1.) instead of showing the form for the proper duration of time, it briefly flashes it on the screen, and 2.) it outputs the original Here instead of There. If I close the ISE and re-open it, the script runs as expected.
What is going on?
tl;dr:
The timer instance is created in the session scope,
whether or not you run your script in the ISE,
and whether or not any variables that reference it are in scope.
Always dispose of a timer (or at least disable it) to prevent it from generating more events.
Generally - although that is not the cause of the problem at hand - be aware that running a script in the ISE implicitly dot-sources it, so that repeated executions run in the same scope, with variable values from previous ones lingering, which can lead to unexpected behavior.
Your code never disposes of (or disables) the timer, which therefore:
stays alive for the entire session, whether or not a variable references it
continues to generate events,
but they only fire while a form is being displayed.
This explains your symptom: The queued up, original events fire instantly as soon as you display the form again.
The solution is to dispose of the timer once it has done its duty and fired the event (once):
Add-Type -AssemblyName System.Windows.Forms
$Form = New-Object System.Windows.Forms.Form
$Timer = New-Object System.Windows.Forms.Timer
$Timer.Add_Tick({
& {
Write-Output "Here"
$Form.Close()
} | Write-Host
})
$Timer.Interval = 3000
$Timer.Start()
$result = $Form.ShowDialog()
$Timer.Dispose() # IMPORTANT: Dispose of the timer so it won't generate more events.
Even with the implicit sourcing behavior of the ISE described above, repeated invocations of this code work as expected.
I think it has to do with how variables in the ISE are still in memory even after the script ends. If you add
$Timer.Stop()
to the last line of the script then close and reopen the ISE it will work.

How to perform keystroke inside powershell?

I have ps1 script to grab some information from the vmware cluster environment.
In some place of ps1 script requires the ENTER button keystroke.
So, How to do that ?
-Thanks
If I understand correctly, you want PowerShell to send the ENTER keystroke to some interactive application?
$wshell = New-Object -ComObject wscript.shell;
$wshell.AppActivate('title of the application window')
Sleep 1
$wshell.SendKeys('~')
If that interactive application is a PowerShell script, just use whatever is in the title bar of the PowerShell window as the argument to AppActivate (by default, the path to powershell.exe). To avoid ambiguity, you can have your script retitle its own window by using the title 'new window title' command.
A few notes:
The tilde (~) represents the ENTER keystroke. You can also use {ENTER}, though they're not identical - that's the keypad's ENTER key. A complete list is available here: http://msdn.microsoft.com/en-us/library/office/aa202943%28v=office.10%29.aspx.
The reason for the Sleep 1 statement is to wait 1 second because it takes a moment for the window to activate, and if you invoke SendKeys immediately, it'll send the keys to the PowerShell window, or to nowhere.
Be aware that this can be tripped up, if you type anything or click the mouse during the second that it's waiting, preventing to window you activate with AppActivate from being active. You can experiment with reducing the amount of time to find the minimum that's reliably sufficient on your system (Sleep accepts decimals, so you could try .5 for half a second). I find that on my 2.6 GHz Core i7 Win7 laptop, anything less than .8 seconds has a significant failure rate. I use 1 second to be safe.
IMPORTANT WARNING: Be extra careful if you're using this method to send a password, because activating a different window between invoking AppActivate and invoking SendKeys will cause the password to be sent to that different window in plain text!
Sometimes wscript.shell's SendKeys method can be a little quirky, so if you run into problems, replace the fourth line above with this:
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait('~');
function Do-SendKeys {
param (
$SENDKEYS,
$WINDOWTITLE
)
$wshell = New-Object -ComObject wscript.shell;
IF ($WINDOWTITLE) {$wshell.AppActivate($WINDOWTITLE)}
Sleep 1
IF ($SENDKEYS) {$wshell.SendKeys($SENDKEYS)}
}
Do-SendKeys -WINDOWTITLE Print -SENDKEYS '{TAB}{TAB}'
Do-SendKeys -WINDOWTITLE Print
Do-SendKeys -SENDKEYS '%{f4}'
Send "Enter" key to an App, for example for pressing "OK". Works great:
Add-Type -AssemblyName microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms
# Get the desired process:
$ProcessName = Get-Process -Name Calculator
Start-Sleep -Seconds 1
# If the process is actually running, bring it to front:
If ($ProcessName)
{
(New-Object -ComObject.Wscript.Shell).AppActivate((Get-Process $ProcessName -ErrorAction SilentlyContinue).MainWindowTitle)
}
# Send "Enter" key to the app:
[Microsoft.VisualBasic.Interaction]::AppActivate($ProcessName.ProcessName)
[System.Windows.Forms.SendKeys]::SendWait({'~'})
Also the $wshell = New-Object -ComObject wscript.shell; helped a script that was running in the background, it worked fine with just but adding $wshell. fixed it from running as background! [Microsoft.VisualBasic.Interaction]::AppActivate("App Name")

How do I trap signals in PowerShell?

Is this possible? I've finally decided to start setting up my personal .NET development environment to closer mimic how I'd set up a *NIX dev environment, which means learning Powershell in earnest.
I'm currently writing a function that recurses through the file system, setting the working directory as it goes in order to build things. One little thing that bothers me is that if I Ctrl+C out of the function, it leaves me wherever the script last was. I've tried setting a trap block that changes the dir to the starting point when run, but this seems to only be intended (and fire) on Exception.
If this were in a language that had root in Unix, I'd set up a signal handler for SIGINT, but I can't find anything similar searching in Powershell. Putting on my .NET cap, I'm imagining there's some sort of event that I can attach a handler to, and if I had to guess, it'd be an event of $host, but I can't find any canonical documentation for System.Management.Automation.Internal.Host.InternalHostUserInterface, and nothing anecdotal that I've been able to search for has been helpful.
Perhaps I'm missing something completely obvious?
Do you mean something like this?
try
{
Push-Location
Set-Location "blah"
# Do some stuff here
}
finally
{
Pop-Location
}
See documentation here. Particularly that paragraph: "The Finally block statements run regardless of whether the Try block encounters a terminating error. Windows PowerShell runs the Finally block before the script terminates or before the current block goes out of scope. A Finally block runs even if you use CTRL+C to stop the script. A Finally block also runs if an Exit keyword stops the script from within a Catch block."
This handles console kepboard input. If control C is pressed during the loop you'll have a chance to handle the event however you want. In the example code a warning is printed and the loop is exited.
[console]::TreatControlCAsInput = $true
dir -Recurse -Path C:\ | % {
# Process file system object here...
Write-Host $_.FullName
# Check if ctrl+C was pressed and quit if so.
if ([console]::KeyAvailable) {
$key = [system.console]::readkey($true)
if (($key.modifiers -band [consolemodifiers]"control") -and ($key.key -eq "C")) {
Write-Warning "Quitting, user pressed control C..."
break
}
}