如何使PowerShell脚本在按下CTRL+C时提供YES或NO选项,而不是直接关闭进程?

huangapple go评论124阅读模式
英文:

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?

问题

  1. 如果存在一个已经计划的关机请求,脚本会在按下Ctrl-C键时自动取消请求。但我不希望它直接取消。在取消之前,我想让它给我一个选项:"您确定要取消现有的关机倒计时吗(Y/N)?"。

  2. 在当前脚本中按下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.

  1. # Determine the path to a file in which information about a pending
  2. # shutdown is persisted by this script.
  3. $lastScheduleFile = Join-Path $env:TEMP ('~{0}_Schedule.txt' -f [IO.Path]::GetFileNameWithoutExtension($PSCommandPath))
  4. [datetime] $shutdownTime = 0
  5. # First, see if this script previously scheduled a shutdown.
  6. try {
  7. $shutdownTime = ([datetime] (Get-Content -ErrorAction Ignore $lastScheduleFile)).ToUniversalTime()
  8. }
  9. catch {}
  10. # If the time is in the past, by definition it doesn't reflect the true pending shutdown time, so we ignore it.
  11. if ($shutdownTime -lt [datetime]::UtcNow) {
  12. $shutdownTime = 0
  13. }
  14. else {
  15. # Warn that the retrieved shutdown time isn't *guaranteed* to be correct.
  16. Write-Warning @'
  17. The pending shutdown time is assumed to be what *this* script last requested,
  18. which is not guaranteed to be the true time, nor is it guaranteed that a shutdown is even still pending.
  19. '@
  20. }
  21. $shutdownAlreadyPending = $shutdownTime -ne 0
  22. if (-not $shutdownAlreadyPending) {
  23. # Prompt the user for when (how many minutes / hours and minutes from now) to shut down.
  24. while ($true) {
  25. try {
  26. $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()) {
  27. '^[1-9]\d*$' { [int] $_ * 60; break }
  28. '^\d+:\d+$' { ([timespan] $_).TotalSeconds; break }
  29. default { throw }
  30. }
  31. break # input was valid; proceed below.
  32. }
  33. catch {
  34. Write-Warning 'Invalid timespan entered; please try again.'
  35. }
  36. }
  37. # Calculate the resulting shutdown time.
  38. $shutdownTime = [datetime]::UtcNow.AddSeconds($secsFromNow)
  39. # Schedule the shutdown via shutdown.exe
  40. while ($true) {
  41. # Note: Due to use of /t with a nonzero value, /f is implied,
  42. # i.e. the shutdown will be forced at the implied time.
  43. shutdown /s /t $secsFromNow
  44. if ($LASTEXITCODE -eq 1190) {
  45. # A shutdown/restart is already scheduled. We cannot know what its delay is.
  46. Write-Warning "A shutdown is already pending. It will be canceled and rescheduled as requsted."
  47. shutdown /a # Abort the pending shutdown, so that the new one can be requested as scheduled.
  48. continue
  49. }
  50. break
  51. }
  52. if ($LASTEXITCODE) {
  53. # Unexpected error.
  54. Write-Error 'Scheduling a shutdown failed unexpectedly.'
  55. exit $LASTEXITCODE
  56. }
  57. # Persist the scheduled shutdown time in a file, so that
  58. # if this script gets killed, we can resume the countdown on re-execution.
  59. $shutdownTime.ToString('o') > $lastScheduleFile
  60. }
  61. # Show a countdown display or handle a preexisting shutdown request,
  62. # with support for Ctrl-C in order to cancel.
  63. $ctrlCPressed = $true
  64. try {
  65. [Console]::CursorVisible = $false
  66. # Display a countdown to the shutdown.
  67. do {
  68. $secsRemaining = ($shutdownTime - [datetime]::UtcNow).TotalSeconds
  69. $timespanRemaining = $shutdownTime - [datetime]::UtcNow
  70. Write-Host -NoNewline ("`r" + 'SHUTTING DOWN in {0:hh\:mm\:ss}, at {1}. Press Ctrl-C to CANCEL.' -f $timespanRemaining, $shutdownTime.ToLocalTime())
  71. Start-Sleep -Seconds 1
  72. } while ($secsRemaining -gt 0)
  73. # Getting here means that Ctrl-C was NOT pressed.
  74. $ctrlCPressed = $false
  75. }
  76. finally {
  77. # Note: Only Write-Host statements can be used in this block.
  78. [Console]::CursorVisible = $true
  79. if ($ctrlCPressed) {
  80. # Abort the pending shutdown.
  81. shutdown /a *>$null
  82. switch ($LASTEXITCODE) {
  83. 0 { Write-Host "`nShutdown aborted by user request." }
  84. 1116 { Write-Host "`n(Shutdown has already been canceled.)" }
  85. default { Write-Host "`nUNEXPECTED ERROR trying to cancel the pending shutdown."; exit $_ }
  86. }
  87. }
  88. # Clean up the file in which the last schedule attempt is persisted.
  89. Remove-Item -ErrorAction Ignore $lastScheduleFile
  90. # Note: We consider this way of exiting successful.
  91. # If the shutdown is allowed to take place, this script never returns to a caller.
  92. # If it *does* return:
  93. # * If it is due to a *failure to even schedule* the shutdown (see above), it will be nonzero.
  94. # * 0 therefore implies having successfully aborted (canceled) the shutdown.
  95. exit 0
  96. }

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

以下是翻译的内容:


  1. &lt;!-- language-all: sh --&gt;
  2. As noted in [the answer that you took the code in the question from](https://stackoverflow.com/a/76625093/45375):
  3. * Handling &lt;kbd&gt;Ctrl-C&lt;/kbd&gt; 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.
  4. * If you need to _intercept_ &lt;kbd&gt;Ctrl-C&lt;/kbd&gt; 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.
  5. This approach also means that the batch file that you're calling the PowerShell script from won't see the &lt;kbd&gt;Ctrl-C&lt;/kbd&gt; 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)).
  6. Here's a **simplified proof of concept** that shows the core technique; it loops for ca. 10 seconds, during which it detects &lt;kbd&gt;Ctrl-C&lt;/kbd&gt; keypresses:
  7. ```powershell
  8. try {
  9. # Make sure that [Console]::ReadKey() can read Ctrl-C as a regular keypress
  10. # instead of getting terminated by it.
  11. [Console]::TreatControlCAsInput = $true
  12. foreach ($i in 1..100) { # Sample loop that runs for ca. 10 secs.
  13. Write-Host -NoNewline . # Visualize the passage of time.
  14. # Check for any keyboard input.
  15. if ([Console]::KeyAvailable) {
  16. # Consume the key without displaying it.
  17. $key = [Console]::ReadKey($true)
  18. # Check if it represents Ctrl-C
  19. $ctrlCPressed = $key.Modifiers -eq 'Control' -and $key.Key -eq 'C'
  20. if ($ctrlCPressed) {
  21. Write-Verbose -Verbose 'Ctrl-C was pressed'
  22. # ... take action here.
  23. }
  24. }
  25. # Sleep a little, but not too long, so that
  26. # keyboard input checking remains responsive.
  27. Start-Sleep -Milliseconds 100
  28. }
  29. }
  30. finally {
  31. # Restore normal Ctrl-C behavior.
  32. [Console]::TreatControlCAsInput = $false
  33. }

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.
  1. # Determine the path to a file in which information about a pending
  2. # shutdown is persisted by this script.
  3. $lastScheduleFile = Join-Path $env:TEMP ('~{0}_Schedule.txt' -f [IO.Path]::GetFileNameWithoutExtension($PSCommandPath))
  4. [datetime] $shutdownTime = 0
  5. # First, see if this script previously scheduled a shutdown.
  6. try {
  7. $shutdownTime = ([datetime] (Get-Content -ErrorAction Ignore $lastScheduleFile)).ToUniversalTime()
  8. }
  9. catch {}
  10. # If the time is in the past, by definition it doesn't reflect the true pending shutdown time, so we ignore it.
  11. if ($shutdownTime -lt [datetime]::UtcNow) {
  12. $shutdownTime = 0
  13. }
  14. else {
  15. # Warn that the retrieved shutdown time isn't *guaranteed* to be correct.
  16. Write-Warning @'
  17. The pending shutdown time is assumed to be what *this* script last requested,
  18. which is not guaranteed to be the true time, nor is it guaranteed that a shutdown is even still pending.
  19. '@
  20. }
  21. $shutdownAlreadyPending = $shutdownTime -ne 0
  22. # Loop for potential scheduling and rescheduling
  23. do {
  24. if (-not $shutdownAlreadyPending -or $reschedule) {
  25. # Prompt the user for when (how many minutes / hours and minutes from now) to shut down.
  26. # Note: Pressing Ctrl-C at this prompt aborts the entire script instantly.
  27. while ($true) {
  28. try {
  29. $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()) {
  30. '^[1-9]\d*$' { [int] $_ * 60; break }
  31. '^\d+:\d+$' { ([timespan] $_).TotalSeconds; break }
  32. default { throw }
  33. }
  34. break # input was valid; proceed below.
  35. }
  36. catch {
  37. Write-Warning 'Invalid timespan entered; please try again.'
  38. }
  39. }
  40. $shutdownTime = [datetime]::UtcNow.AddSeconds($secsFromNow)
  41. # Schedule the shutdown via shutdown.exe
  42. while ($true) {
  43. # Note: Due to use of /t with a nonzero value, /f is implied,
  44. # i.e. the shutdown will be forced at the implied time.
  45. shutdown /s /t $secsFromNow
  46. if ($LASTEXITCODE -eq 1190) {
  47. # A shutdown/restart is already scheduled. We cannot know for what time.
  48. Write-Warning "A shutdown is already pending. It will be canceled and rescheduled as reqeusted."
  49. shutdown /a # Abort the pending shutdown, so that the new one can be requested as scheduled.
  50. continue
  51. }
  52. break
  53. }
  54. if ($LASTEXITCODE) {
  55. # Unexpected error.
  56. Write-Error 'Scheduling the shutdown failed unexpectedly.'
  57. exit $LASTEXITCODE
  58. }
  59. # Persist the scheduled shutdown time in a file, so that
  60. # if this script gets killed, we can resume the countdown on re-execution.
  61. $shutdownTime.ToString('o') > $lastScheduleFile
  62. }
  63. # Show a countdown display, with support for Ctrl-C in order to cancel.
  64. $ctrlCPressed = $reschedule = $canceled = $false
  65. $prevSecsRemaining = 0
  66. try {
  67. # Make sure that [Console]::ReadKey() can read Ctrl-C as a regular keypress
  68. # instead of getting terminated by it.
  69. [Console]::TreatControlCAsInput = $true
  70. [Console]::CursorVisible = $false
  71. do {
  72. $timespanRemaining = $shutdownTime - [datetime]::UtcNow
  73. if ([int] $timespanRemaining.TotalSeconds -ne $prevSecs
  74. <details>
  75. <summary>英文:</summary>
  76. &lt;!-- language-all: sh --&gt;
  77. As noted in [the answer that you took the code in the question from](https://stackoverflow.com/a/76625093/45375):
  78. * Handling &lt;kbd&gt;Ctrl-C&lt;/kbd&gt; 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.
  79. * If you need to _intercept_ &lt;kbd&gt;Ctrl-C&lt;/kbd&gt; so that you can opt to _prevent_ termination, you&#39;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.
  80. This approach also means that the batch file that you&#39;re calling the PowerShell script from won&#39;t see the &lt;kbd&gt;Ctrl-C&lt;/kbd&gt; keypress, and therefore won&#39;t show the dreaded `Terminate batch job (Y/N)?` prompt, which cannot be suppressed (see [this answer](https://stackoverflow.com/a/50892407/45375)).
  81. Here&#39;s a **simplified proof of concept** that shows the core technique; it loops for ca. 10 seconds, during which it detects &lt;kbd&gt;Ctrl-C&lt;/kbd&gt; 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
}

  1. The **full solution** in the context of your code:
  2. * 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) {

  1. # Prompt the user for when (how many minutes / hours and minutes from now) to shut down.
  2. # Note: Pressing Ctrl-C at this prompt aborts the entire script instantly.
  3. while ($true) {
  4. try {
  5. $secsFromNow = switch -Regex ((Read-Host &#39;Enter the timespan after which to shut down, either in minutes (e.g. 30) or hours and minutes (e.g. 1:15)&#39;).Trim()) {
  6. &#39;^[1-9]\d*$&#39; { [int] $_ * 60; break }
  7. &#39;^\d+:\d+$&#39; { ([timespan] $_).TotalSeconds; break }
  8. default { throw }
  9. }
  10. break # input was valid; proceed below.
  11. }
  12. catch {
  13. Write-Warning &#39;Invalid timespan entered; please try again.&#39;
  14. }
  15. }
  16. $shutdownTime = [datetime]::UtcNow.AddSeconds($secsFromNow)
  17. # Schedule the shutdown via shutdown.exe
  18. while ($true) {
  19. # Note: Due to use of /t with a nonzero value, /f is implied,
  20. # i.e. the shutdown will be forced at the implied time.
  21. shutdown /s /t $secsFromNow
  22. if ($LASTEXITCODE -eq 1190) {
  23. # A shutdown/restart is already scheduled. We cannot know for what time.
  24. Write-Warning &quot;A shutdown is already pending. It will be canceled and rescheduled as reqeusted.&quot;
  25. shutdown /a # Abort the pending shutdown, so that the new one can be requested as scheduled.
  26. continue
  27. }
  28. break
  29. }
  30. if ($LASTEXITCODE) {
  31. # Unexpected error.
  32. Write-Error &#39;Scheduling the shutdown failed unexpectedly.&#39;
  33. exit $LASTEXITCODE
  34. }
  35. # Persist the scheduled shutdown time in a file, so that
  36. # if this script gets killed, we can resume the countdown on re-execution.
  37. $shutdownTime.ToString(&#39;o&#39;) &gt; $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 {

  1. $timespanRemaining = $shutdownTime - [datetime]::UtcNow
  2. if ([int] $timespanRemaining.TotalSeconds -ne $prevSecsRemaining) {
  3. # Update only if the seconds position changed.
  4. Write-Host -NoNewline (&quot;`r&quot; + &#39;SHUTTING DOWN in {0:hh\:mm\:ss}, at {1}. Press Ctrl-C to CANCEL.&#39; -f $timespanRemaining, $shutdownTime.ToLocalTime())
  5. $prevSecsRemaining = [int] $timespanRemaining.TotalSeconds
  6. }
  7. # Check for Ctrl-C
  8. if ([Console]::KeyAvailable) {
  9. # A keypress is available, consume it without displaying it.
  10. $key = [Console]::ReadKey($true)
  11. # Check if it represents Ctrl-C.
  12. $ctrlCPressed = $key.Modifiers -eq &#39;Control&#39; -and $key.Key -eq &#39;C&#39;
  13. if ($ctrlCPressed) {
  14. # Use choice.exe to prompt for further action.
  15. choice.exe /m &quot;`nAre you sure you want to cancel the existing countdown to shutdown? Press N to reschedule instead, or R to resume the current countdown.&quot; /c ynr
  16. # Evaluate the user&#39;s choice, which is reflected in the exit code
  17. # and therefore in the automatic $LASTEXITCODE variable.
  18. # 1 indicates the first choice (&#39;y&#39;), 2 the second (&#39;n&#39;)
  19. switch ($LASTEXITCODE) {
  20. 1 {
  21. # YES: Cancel the shutdown and exit
  22. Write-Host &#39;Canceling shutdown.&#39;
  23. $canceled = $true
  24. break
  25. }
  26. 2 {
  27. # NO: Cancel the shutdown, but schedule a new one.
  28. Write-Host &#39;Canceling current shutdown. Please schedule a new one now:&#39;;
  29. $reschedule = $true
  30. }
  31. 3 {
  32. # RESUME: keep the current schedule and keep counting down.
  33. }
  34. Default {
  35. # With three choices, this would imply a value of 0, which would signal having pressed Ctrl-C again.
  36. # Due to [Console]::TreatControlCAsInput = $false, however, you won&#39;t get here.
  37. }
  38. }
  39. if ($reschedule -or $canceled) { break }
  40. }
  41. }
  42. # Sleep only for a short while, so that checking for keyboard input is responsive.
  43. Start-Sleep -Milliseconds 100
  44. } 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) {

  1. # Cancel the pending shutdown
  2. shutdown /a *&gt;$null
  3. switch ($LASTEXITCODE) {
  4. 0 { &lt;# OK #&gt; }
  5. 1116 { &lt;# Shutdown has unexpectedly already been canceled, but that is a benign condition #&gt; }
  6. default { Write-Host &quot;`nUNEXPECTED ERROR trying to cancel the pending shutdown.&quot;; exit $_ }
  7. }

}

} while ($reschedule)

Exit the script here: either the shutdown has begun, or it has been canceled without replacement.

exit

  1. </details>

huangapple
  • 本文由 发表于 2023年7月7日 02:21:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/76631574.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定