英文:
How do I get the PowerShell Scriptin to give me a YES or NO option when I press CTRL+C instead of closing the process directly?
问题
-
如果存在一个已经计划的关机请求,脚本会在按下Ctrl-C键时自动取消请求。但我不希望它直接取消。在取消之前,我想让它给我一个选项:"您确定要取消现有的关机倒计时吗(Y/N)?"。
-
在当前脚本中按下Ctrl-C键后,关机请求会被取消,并且终端会显示以下警告:
"Shutdown aborted by user request.
Terminate batch job (Y/N)?"
在选择并输入Y后,终端会关闭。但是,在以相同方式选择并输入N后,终端也会关闭。
在这一点上,我需要这样的功能。如果我选择N选项,终端将不会关闭,并要求我输入新的关机请求。换句话说,应该在当前关机取消后给我设置新的关机选项的机会。
如果有人对此有了解,我想让他知道我由衷地表示感谢。
英文:
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 <kbd>Ctrl-C</kbd> 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 <kbd>Ctrl-C</kbd> 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 <kbd>Y</kbd>, the terminal is closed.
However, the terminal is also closed after selecting and entering <kbd>N</kbd> in the same way.
At this point I need this.
If I select the <kbd>N</kbd> 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.
答案1
得分: 1
以下是翻译的内容:
<!-- language-all: sh -->
As noted in [the answer that you took the code in the question from](https://stackoverflow.com/a/76625093/45375):
* Handling <kbd>Ctrl-C</kbd> via the `finally` block of a [`try` / `catch` / `finally` statement](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Try_Catch_Finally) in PowerShell restricts to you to _cleanup_ operations - your script will _invariably_ terminate.
* If you need to _intercept_ <kbd>Ctrl-C</kbd> so that you can opt to _prevent_ termination, you'll need a custom keyboard-polling loop with [`[Console]::TreatControlCAsInput`](https://learn.microsoft.com/en-US/dotnet/api/System.Console.TreatControlCAsInput)`= $true`, as shown in [this answer](https://stackoverflow.com/a/16408863/45375), 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 <kbd>Ctrl-C</kbd> keypress, and therefore won't show the dreaded `Terminate batch job (Y/N)?` prompt, which cannot be suppressed (see [this answer](https://stackoverflow.com/a/50892407/45375)).
Here's a **simplified proof of concept** that shows the core technique; it loops for ca. 10 seconds, during which it detects <kbd>Ctrl-C</kbd> keypresses:
```powershell
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:
- Note that it includes a
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 $prevSecs
<details>
<summary>英文:</summary>
<!-- language-all: sh -->
As noted in [the answer that you took the code in the question from](https://stackoverflow.com/a/76625093/45375):
* Handling <kbd>Ctrl-C</kbd> via the `finally` block of a [`try` / `catch` / `finally` statement](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Try_Catch_Finally) in PowerShell restricts to you to _cleanup_ operations - your script will _invariably_ terminate.
* If you need to _intercept_ <kbd>Ctrl-C</kbd> so that you can opt to _prevent_ termination, you'll need a custom keyboard-polling loop with [`[Console]::TreatControlCAsInput`](https://learn.microsoft.com/en-US/dotnet/api/System.Console.TreatControlCAsInput)`= $true`, as shown in [this answer](https://stackoverflow.com/a/16408863/45375), 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 <kbd>Ctrl-C</kbd> keypress, and therefore won't show the dreaded `Terminate batch job (Y/N)?` prompt, which cannot be suppressed (see [this answer](https://stackoverflow.com/a/50892407/45375)).
Here's a **simplified proof of concept** that shows the core technique; it loops for ca. 10 seconds, during which it detects <kbd>Ctrl-C</kbd> 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:
* Note that it includes a `R` (Resume) [`choice.exe`](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/choice) 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
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论