The PowerShell ISE sometimes behaves unpredictably after code changes are made - powershell

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.

Related

Make Powershell script continue running to next task whilst displaying a countdown timer

I'm trying to execute a script in Powershell that has "time left" to run a script but also want it to continue with the next task (executing policies to Users and exporting results to .csv). The Timer must also stop when the script Ends. Currently, the time counts down to 0, then the script continues. The Script works but I want to be able to execute 2 processes at once. Please help.
Above the screenshot, Timer Counts down 1st before continuing to process the next part of the script. How do we get it to run both at once?
Once the time has counted down to 0, then the above process runs.
$Variable = Import-CSV -Path
"C:\Users\MicrosoftTeams\locationbasedpolicy1.csv"
$ApproxTime = 10
for ($seconds=$ApproxTime; $seconds -gt -1; $seconds--){
Write-Host -NoNewLine ("`rTime Left: " + ("{0:d4}" -f $seconds))
Start-Sleep -Seconds 1
}
foreach ($U in $Variable){
$user = $U.userPrincipalName
Grant-CSTeamsEmergencyCallingPolicy -policyname ECP-UK-All-Users
-identity $user
Write-Host "$($user) has been assigned Policy!"
}
Note: Your screenshot implies that you're using PowerShell (Core) 7+, which in turn implies that you can use Start-ThreadJob for thread-based background jobs.
With notable limitations - which may or may not be acceptable - you can do the following:
Use timer events to update your countdown display message, on a fixed console line that is temporarily restored.
Note: The solution below uses the line that would become the next output line in the course of your script's display output. This means that when the window content scrolls as more output that can fit on a single screen is produced. A better approach would be to pick a fixed line - at the top or the bottom of the window - but doing so would require saving and restoring the previous content of that line.
Run the commands via Start-ThreadJob, relaying their output; running in a separate thread is necessary in order for the countdown display in the foreground thread to update relatively predictably.
The problem with progress messages being emitted from a thread job is that they print on successive lines for every Receive-Job call - and there is no workaround that I'm aware of.
You can forgo progress messages by setting $ProgressPreference = 'SilentlyContinue' in the thread job's script block, but you'll obviously lose detailed progress feedback; the best you could do is provide a "still alive" kind of progress message in the foreground loop.
The following self-contained sample code shows this technique:
Even if the limitations are acceptable, the effort is nontrivial.
As an example command that shows progress messages, Invoke-WebRequest is used to download a file (the file is cleaned up afterwards).
$countDownSecs = 10
# Create a timer that will fire every second once started.
$timer = [System.Timers.Timer]::new(1000)
# Register a handler for the timer's "Elapsed" event, and pass the
# the current cursor position and the projected end timestamp to the -Action script block.
$eventJob = Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {
# Save the current cursor position
$currCursorPos = $host.UI.RawUI.CursorPosition
# Restore the cursor position for the countdown message.
$host.UI.RawUI.CursorPosition = $Event.MessageData.CountDownCursorPos
[Console]::Write((
'Time Left: {0:d4}' -f [int] ($Event.MessageData.CountDownTo - [datetime]::UtcNow).TotalSeconds
))
# Restore the cursor position
$host.UI.RawUI.CursorPosition = $currCursorPos
} -MessageData #{
CountDownCursorPos = $host.UI.RawUI.CursorPosition
CountDownTo = [datetime]::UtcNow.AddSeconds($countDownSecs)
}
# Write the initial countdown message now, both for instand feedback and to
# "reserve" the console line for future updates.
Write-Host ('Time Left: {0:d4}' -f $countDownSecs)
# Start the timer.
$timer.Start()
# Run a command that uses progress display (Write-Progress)
# in a parallel thread with Start-ThreadJob and track its
# progress.
try {
$tmpFile = New-TemporaryFile
# Run the command(s) in a thread job.
# That way, the timer events are processed relatively predictably in the foreground thread.
$jb = Start-ThreadJob {
# Activate the next line to silence progress messages.
# $ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -OutFile $using:tmpFile https://github.com/PowerShell/PowerShell/releases/download/v7.2.6/powershell-lts_7.2.6-1.deb_amd64.deb
}
# Wait for the thread job to finish, relaying output on an ongoing basis.
while ($jb.State -in 'NotStarted', 'Running') {
# Note: While Receive-Job is necessary even for progress messages, such messages:
# (a) cannot be captured or redirected
# (b) invariably print on successive lines for every Receive-Job call;
# even trying to restore the cursor position after every Receive-Job call doesn't help.
$jb | Receive-Job
Start-Sleep -Milliseconds 50
}
} finally {
# Stop timer and remove the jobs and the temp. file
$timer.Stop()
$jb, $eventJob | Remove-Job -Force
$tmpFile | Remove-Item
}

Start Internet Explorer, wait until the user closes it, then exit the script

I have a script that opened IE with no toolbar and to navigate to a url (below)
Dim IE, URL
URL = "website.com"
Set IE = CreateObject("InternetExplorer.Application")
IE.Navigate URL
IE.Visible = 1
IE.ToolBar = 0
IE.Left = 0
I need to have the window appear like this for reasons,
I cant use kiosk mode.
What I need to do is:
run the script (LaunchIE_noToolbar.vbs)
open IE, hide toolbar, go to url
wait in the background while the user does stuff which might be 10-15 minutes later
recognize it's closed and exit the script
It seems simple enough, but I can't work it out, the waiting part is driving me nuts.
If you can do this in PowerShell that would be great too, I don't mind.
I'm not in front of a computer right now, but as I recall...
You can start a process aka start-process I think. Grab the process ID which is returned. Then wait for the process to complete, might be wait-process? Do a "help process" at the PowerShell prompt for commands and syntax.
There may be a more elegant PowerShell solution, but this seems to work:
$url = "website.com"
$ie = New-Object -ComObject "InternetExplorer.Application"
$ie.Navigate($url)
$ie.Visible = 1
$ie.ToolBar = 0
$ie.Left = 0
# Wait for IE to exit.
while ($null -ne $ie.ReadyState) { # .ReadyState is only $null once the IE instance exits
Start-Sleep -Milliseconds 100
}

How to check if NumLock is enabled

Recently I ran into an issue with a laptop that had NumLock disabled automatically at certain times (such as when coming out of sleep mode). This prompted me to look for ways to programmatically check if NumLock was off, and if so, turn it on.
I'm looking for the best way to accomplish this. I want to run the script when certain events occur, such as when logging on to the laptop. I plan to do this with a scheduled task, and I'd prefer to use PowerShell over VBScript, but I'd be happy to use whatever works.
In PowerShell, [console]::NumberLock is a Read Only property that will evaluate to true if NumLock is enabled.
The script I ended up writing is as follows:
if(-not [console]::NumberLock){
$w = New-Object -ComObject WScript.Shell;
$w.SendKeys('{NUMLOCK}');
}
Building on Thriggle's answer above, you can ensure NumLock never gets turned off by adding a loop. Note that this is pretty hard on CPU:
While($true){
if(-not [console]::NumberLock){
$w = New-Object -ComObject WScript.Shell;
$w.SendKeys('{NUMLOCK}');
}
}

Strange function call behavior with respect to .NET events

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 :)

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")