I am using the following PowerShell Script to be able to shut down the computer after a certain time.
# Determine the path to a file in which information about a pending
# shutdown is persisted by this script.
$lastScheduleFile = Join-Path $env:TEMP ('~{0}_Schedule.txt' -f [IO.Path]::GetFileNameWithoutExtension($PSCommandPath))
[datetime] $shutdownTime = 0
# First, see if this script previously scheduled a shutdown.
try {
$shutdownTime = ([datetime] (Get-Content -ErrorAction Ignore $lastScheduleFile)).ToUniversalTime()
}
catch {}
# If the time is in the past, by definition it doesn't reflect the true pending shutdown time, so we ignore it.
if ($shutdownTime -lt [datetime]::UtcNow) {
$shutdownTime = 0
}
else {
# Warn that the retrieved shutdown time isn't *guaranteed* to be correct.
Write-Warning @'
The pending shutdown time is assumed to be what *this* script last requested,
which is not guaranteed to be the true time, nor is it guaranteed that a shutdown is even still pending.
'@
}
$shutdownAlreadyPending = $shutdownTime -ne 0
if (-not $shutdownAlreadyPending) {
# Prompt the user for when (how many minutes / hours and minutes from now) to shut down.
while ($true) {
try {
$secsFromNow = switch -Regex ((Read-Host 'Enter the timespan after which to shut down, either in minutes (e.g. 30) or hours and minutes (e.g. 1:15)').Trim()) {
'^[1-9]\d*$' { [int] $_ * 60; break }
'^\d+:\d+$' { ([timespan] $_).TotalSeconds; break }
default { throw }
}
break # input was valid; proceed below.
}
catch {
Write-Warning 'Invalid timespan entered; please try again.'
}
}
# Calculate the resulting shutdown time.
$shutdownTime = [datetime]::UtcNow.AddSeconds($secsFromNow)
# Schedule the shutdown via shutdown.exe
while ($true) {
# Note: Due to use of /t with a nonzero value, /f is implied,
# i.e. the shutdown will be forced at the implied time.
shutdown /s /t $secsFromNow
if ($LASTEXITCODE -eq 1190) {
# A shutdown/restart is already scheduled. We cannot know what its delay is.
Write-Warning "A shutdown is already pending. It will be canceled and rescheduled as requsted."
shutdown /a # Abort the pending shutdown, so that the new one can be requested as scheduled.
continue
}
break
}
if ($LASTEXITCODE) {
# Unexpected error.
Write-Error 'Scheduling a shutdown failed unexpectedly.'
exit $LASTEXITCODE
}
# Persist the scheduled shutdown time in a file, so that
# if this script gets killed, we can resume the countdown on re-execution.
$shutdownTime.ToString('o') > $lastScheduleFile
}
# Show a countdown display or handle a preexisting shutdown request,
# with support for Ctrl-C in order to cancel.
$ctrlCPressed = $true
try {
[Console]::CursorVisible = $false
# Display a countdown to the shutdown.
do {
$secsRemaining = ($shutdownTime - [datetime]::UtcNow).TotalSeconds
$timespanRemaining = $shutdownTime - [datetime]::UtcNow
Write-Host -NoNewline ("`r" + 'SHUTTING DOWN in {0:hh\:mm\:ss}, at {1}. Press Ctrl-C to CANCEL.' -f $timespanRemaining, $shutdownTime.ToLocalTime())
Start-Sleep -Seconds 1
} while ($secsRemaining -gt 0)
# Getting here means that Ctrl-C was NOT pressed.
$ctrlCPressed = $false
}
finally {
# Note: Only Write-Host statements can be used in this block.
[Console]::CursorVisible = $true
if ($ctrlCPressed) {
# Abort the pending shutdown.
shutdown /a *>$null
switch ($LASTEXITCODE) {
0 { Write-Host "`nShutdown aborted by user request." }
1116 { Write-Host "`n(Shutdown has already been canceled.)" }
default { Write-Host "`nUNEXPECTED ERROR trying to cancel the pending shutdown."; exit $_ }
}
}
# Clean up the file in which the last schedule attempt is persisted.
Remove-Item -ErrorAction Ignore $lastScheduleFile
# Note: We consider this way of exiting successful.
# If the shutdown is allowed to take place, this script never returns to a caller.
# If it *does* return:
# * If it is due to a *failure to even schedule* the shutdown (see above), it will be nonzero.
# * 0 therefore implies having successfully aborted (canceled) the shutdown.
exit 0
}
The script works pretty well, except for a few things; but there are a few things i want.
1) If there is an existing shutdown request, the script automatically cancels the request when we press the Ctrl-C key. But I don't want him to cancel it directly. Before canceling I want it to give me an option "Are you sure you want to cancel the existing countdown to shutdown (Y/N)?:".
2) After pressing the Ctrl-C key in the current script, the shutdown request is canceled and it gives the following warning in the terminal:
Shutdown aborted by user request. Terminate batch job (Y/N)?
After selecting and entering Y, the terminal is closed. However, the terminal is also closed after selecting and entering N in the same way.
At this point I need this. If I select the N option, the terminal will not be closed; and ask me to enter a new shutdown request. In other words; should give me the option to set a new shutdown after the current shutdown is cancelled.
If there is someone who has knowledge on this subject, I would like him to know that I am expressing my gratitude with all my sincerity.
As noted in the answer that you took the code in the question from:
Handling Ctrl-C via the finally
block of a try
/ catch
/ finally
statement in PowerShell restricts to you to cleanup operations - your script will invariably terminate.
If you need to intercept Ctrl-C so that you can opt to prevent termination, you'll need a custom keyboard-polling loop with [Console]::TreatControlCAsInput
= $true
, as shown in this answer, and as spelled out in the context of your code below.
This approach also means that the batch file that you're calling the PowerShell script from won't see the Ctrl-C keypress, and therefore won't show the dreaded Terminate batch job (Y/N)?
prompt, which cannot be suppressed (see this answer).
Here's a simplified proof of concept that shows the core technique; it loops for ca. 10 seconds, during which it detects Ctrl-C keypresses:
try {
# Make sure that [Console]::ReadKey() can read Ctrl-C as a regular keypress
# instead of getting terminated by it.
[Console]::TreatControlCAsInput = $true
foreach ($i in 1..100) { # Sample loop that runs for ca. 10 secs.
Write-Host -NoNewline . # Visualize the passage of time.
# Check for any keyboard input.
if ([Console]::KeyAvailable) {
# Consume the key without displaying it.
$key = [Console]::ReadKey($true)
# Check if it represents Ctrl-C
$ctrlCPressed = $key.Modifiers -eq 'Control' -and $key.Key -eq 'C'
if ($ctrlCPressed) {
Write-Verbose -Verbose 'Ctrl-C was pressed'
# ... take action here.
}
}
# Sleep a little, but not too long, so that
# keyboard input checking remains responsive.
Start-Sleep -Milliseconds 100
}
}
finally {
# Restore normal Ctrl-C behavior.
[Console]::TreatControlCAsInput = $false
}
The full solution in the context of your code:
R
(Resume) choice.exe
option that lets you resume the current shutdown schedule and shutdown.# Determine the path to a file in which information about a pending
# shutdown is persisted by this script.
$lastScheduleFile = Join-Path $env:TEMP ('~{0}_Schedule.txt' -f [IO.Path]::GetFileNameWithoutExtension($PSCommandPath))
[datetime] $shutdownTime = 0
# First, see if this script previously scheduled a shutdown.
try {
$shutdownTime = ([datetime] (Get-Content -ErrorAction Ignore $lastScheduleFile)).ToUniversalTime()
}
catch {}
# If the time is in the past, by definition it doesn't reflect the true pending shutdown time, so we ignore it.
if ($shutdownTime -lt [datetime]::UtcNow) {
$shutdownTime = 0
}
else {
# Warn that the retrieved shutdown time isn't *guaranteed* to be correct.
Write-Warning @'
The pending shutdown time is assumed to be what *this* script last requested,
which is not guaranteed to be the true time, nor is it guaranteed that a shutdown is even still pending.
'@
}
$shutdownAlreadyPending = $shutdownTime -ne 0
# Loop for potential scheduling and rescheduling
do {
if (-not $shutdownAlreadyPending -or $reschedule) {
# Prompt the user for when (how many minutes / hours and minutes from now) to shut down.
# Note: Pressing Ctrl-C at this prompt aborts the entire script instantly.
while ($true) {
try {
$secsFromNow = switch -Regex ((Read-Host 'Enter the timespan after which to shut down, either in minutes (e.g. 30) or hours and minutes (e.g. 1:15)').Trim()) {
'^[1-9]\d*$' { [int] $_ * 60; break }
'^\d+:\d+$' { ([timespan] $_).TotalSeconds; break }
default { throw }
}
break # input was valid; proceed below.
}
catch {
Write-Warning 'Invalid timespan entered; please try again.'
}
}
$shutdownTime = [datetime]::UtcNow.AddSeconds($secsFromNow)
# Schedule the shutdown via shutdown.exe
while ($true) {
# Note: Due to use of /t with a nonzero value, /f is implied,
# i.e. the shutdown will be forced at the implied time.
shutdown /s /t $secsFromNow
if ($LASTEXITCODE -eq 1190) {
# A shutdown/restart is already scheduled. We cannot know for what time.
Write-Warning "A shutdown is already pending. It will be canceled and rescheduled as reqeusted."
shutdown /a # Abort the pending shutdown, so that the new one can be requested as scheduled.
continue
}
break
}
if ($LASTEXITCODE) {
# Unexpected error.
Write-Error 'Scheduling the shutdown failed unexpectedly.'
exit $LASTEXITCODE
}
# Persist the scheduled shutdown time in a file, so that
# if this script gets killed, we can resume the countdown on re-execution.
$shutdownTime.ToString('o') > $lastScheduleFile
}
# Show a countdown display, with support for Ctrl-C in order to cancel.
$ctrlCPressed = $reschedule = $canceled = $false
$prevSecsRemaining = 0
try {
# Make sure that [Console]::ReadKey() can read Ctrl-C as a regular keypress
# instead of getting terminated by it.
[Console]::TreatControlCAsInput = $true
[Console]::CursorVisible = $false
do {
$timespanRemaining = $shutdownTime - [datetime]::UtcNow
if ([int] $timespanRemaining.TotalSeconds -ne $prevSecsRemaining) {
# Update only if the seconds position changed.
Write-Host -NoNewline ("`r" + 'SHUTTING DOWN in {0:hh\:mm\:ss}, at {1}. Press Ctrl-C to CANCEL.' -f $timespanRemaining, $shutdownTime.ToLocalTime())
$prevSecsRemaining = [int] $timespanRemaining.TotalSeconds
}
# Check for Ctrl-C
if ([Console]::KeyAvailable) {
# A keypress is available, consume it without displaying it.
$key = [Console]::ReadKey($true)
# Check if it represents Ctrl-C.
$ctrlCPressed = $key.Modifiers -eq 'Control' -and $key.Key -eq 'C'
if ($ctrlCPressed) {
# Use choice.exe to prompt for further action.
choice.exe /m "`nAre you sure you want to cancel the existing countdown to shutdown? Press N to reschedule instead, or R to resume the current countdown." /c ynr
# Evaluate the user's choice, which is reflected in the exit code
# and therefore in the automatic $LASTEXITCODE variable.
# 1 indicates the first choice ('y'), 2 the second ('n')
switch ($LASTEXITCODE) {
1 {
# YES: Cancel the shutdown and exit
Write-Host 'Canceling shutdown.'
$canceled = $true
break
}
2 {
# NO: Cancel the shutdown, but schedule a new one.
Write-Host 'Canceling current shutdown. Please schedule a new one now:';
$reschedule = $true
}
3 {
# RESUME: keep the current schedule and keep counting down.
}
Default {
# With three choices, this would imply a value of 0, which would signal having pressed Ctrl-C again.
# Due to [Console]::TreatControlCAsInput = $false, however, you won't get here.
}
}
if ($reschedule -or $canceled) { break }
}
}
# Sleep only for a short while, so that checking for keyboard input is responsive.
Start-Sleep -Milliseconds 100
} while ($timespanRemaining -gt 0)
}
finally {
# Clean up / restore settings.
[Console]::CursorVisible = $true
[Console]::TreatControlCAsInput = $false
# Clean up the file in which the last schedule attempt is persisted.
# This is appropriate even if the loop is exited due to having intercepted Ctrl-C
Remove-Item -ErrorAction Ignore $lastScheduleFile
}
if ($reschedule -or $canceled) {
# Cancel the pending shutdown
shutdown /a *>$null
switch ($LASTEXITCODE) {
0 { <# OK #> }
1116 { <# Shutdown has unexpectedly already been canceled, but that is a benign condition #> }
default { Write-Host "`nUNEXPECTED ERROR trying to cancel the pending shutdown."; exit $_ }
}
}
} while ($reschedule)
# Exit the script here: either the shutdown has begun, or it has been canceled without replacement.
exit