Ignore CTRL+C in delay - powershell - powershell

Whenever I set a delay in powershell using Start-Sleep, e.g:
Start-Sleep 10
then it does not ignore CTRLC. I mean when I hit that key stroke the delay quits. How can I ignore it in delays?

You can temporarily set [Console]::TreatControlCAsInput to $true:
[Console]::TreatControlCAsInput = $true
Start-Sleep 10 # Ctrl-C will now not abort this sleep.
[Console]::TreatControlCAsInput = $false

I have found a way to do this:
[System.Threading.Thread]::Sleep(milliseconds)
This ignores CTRL-C on delays.

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
}

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

PowerShell: Toggle "Num Lock" on and off.

I would like to be able toggle on and off the "Num Lock" key on the keyboard. I have tried multiple examples around the web and here, with no success. This is the closest thing I've got to a solution:
[void] [System.Reflection.Assembly]::LoadWithPartialName("'System.Windows.Forms")
[System.Windows.Forms.SendKeys]::SendWait("{NUMLOCK}")
The above code looks like it might work and I see the "Num Lock" indicator on my keyboard flash for a second but it doesn't "stick".
I experienced the same on/off flicker you did.
This works fine for me though, give it a try:
$wsh = New-Object -ComObject WScript.Shell
$wsh.SendKeys('{NUMLOCK}')
For what it is worth, from the perspective of the keyboard, the OS sends a set and then a reset of the toggle key (caps, scroll lock, numlock) when using [System.Windows.Forms.SendKeys]::SendWait("{CAPSLOCK}"), but only sends a single event using WScript.Shell.
Stumbled here looking for a way to keep Num Lock ON eternally (apparently the word "lock" doesn't mean much 🤷‍♂️). This script checks Num Lock status every 2 seconds, and switches it back on if it's been turned off:
$wsh = New-Object -ComObject WScript.Shell
while($true){
if ([console]::NumberLock -eq $false) {
$wsh.SendKeys('{NUMLOCK}')
}
Start-Sleep 2
}
I think it would be a good candidate for converting into a standalone tiny .exe using PS2EXE.
intTime=0
strInputVal=InputBox("Enter the time","Enter Hours in Int")
intTime=strInputVal * 60 * 60
set WshShell = WScript.CreateObject("WScript.Shell")
For i = 1 to intTime
WScript.Sleep 500
WshShell.SendKeys "{NUMLOCK}"
WScript.Sleep 500
Next
WScript.Quit
Set WshShell = Nothing

Gracefully stopping in Powershell

How do I catch and handle Ctrl+C in a PowerShell script? I understand that I can do this from a cmdlet in v2 by including an override for the Powershell.Stop() method, but I can't find an analog for use in scripts.
I'm currently performing cleanup via an end block, but I need to perform additional work when the script is canceled (as opposed to run to completion).
The documentation for try-catch-finally says:
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.
See the following example. Run it and cancel it by pressing ctrl-c.
try
{
while($true)
{
"Working.."
Start-Sleep -Seconds 1
}
}
finally
{
write-host "Ended work."
}
You could use the method described on here on PoshCode
Summary:
Set
[console]::TreatControlCAsInput = $true
then poll for user input using
if($Host.UI.RawUI.KeyAvailable -and (3 -eq
[int]$Host.UI.RawUI.ReadKey("AllowCtrlC,IncludeKeyUp,NoEcho").Character))
There is also a Stopping property on $PSCmdlet that can be used for this.
Here is recent, working solution. I use the if part in a loop where I need to control the execution interruption (closing filehandles).
[Console]::TreatControlCAsInput = $true # at beginning of script
if ([Console]::KeyAvailable){
$readkey = [Console]::ReadKey($true)
if ($readkey.Modifiers -eq "Control" -and $readkey.Key -eq "C"){
# tasks before exit here...
return
}
}
Also note that there is a bug which leads KeyAvailable to be true upon start of scripts. You can mitigate by read calling ReadKey once at start. Not needed for this approach, just worth knowing in this context.