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

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

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.

# 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

以下是翻译的内容:


&lt;!-- language-all: sh --&gt;

As noted in [the answer that you took the code in the question from](https://stackoverflow.com/a/76625093/45375):

* 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.

* 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.

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

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:

```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>

&lt;!-- language-all: sh --&gt;

As noted in [the answer that you took the code in the question from](https://stackoverflow.com/a/76625093/45375):

* 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.

* 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.

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

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
}


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 &#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()) {
&#39;^[1-9]\d*$&#39; { [int] $_ * 60; break }
&#39;^\d+:\d+$&#39; { ([timespan] $_).TotalSeconds; break }
default { throw }
}
break # input was valid; proceed below.
}
catch {
Write-Warning &#39;Invalid timespan entered; please try again.&#39;
}
}
$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 &quot;A shutdown is already pending. It will be canceled and rescheduled as reqeusted.&quot;
shutdown /a # Abort the pending shutdown, so that the new one can be requested as scheduled.
continue
}
break
}
if ($LASTEXITCODE) {
# Unexpected error.
Write-Error &#39;Scheduling the shutdown failed unexpectedly.&#39;
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(&#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 {

  $timespanRemaining = $shutdownTime - [datetime]::UtcNow
if ([int] $timespanRemaining.TotalSeconds -ne $prevSecsRemaining) {
# Update only if the seconds position changed.
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())
$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 &#39;Control&#39; -and $key.Key -eq &#39;C&#39;
if ($ctrlCPressed) {
# Use choice.exe to prompt for further action.
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
# Evaluate the user&#39;s choice, which is reflected in the exit code
# and therefore in the automatic $LASTEXITCODE variable.
# 1 indicates the first choice (&#39;y&#39;), 2 the second (&#39;n&#39;)
switch ($LASTEXITCODE) {
1 {
# YES: Cancel the shutdown and exit
Write-Host &#39;Canceling shutdown.&#39;
$canceled = $true
break 
}  
2 {
# NO: Cancel the shutdown, but schedule a new one.
Write-Host &#39;Canceling current shutdown. Please schedule a new one now:&#39;;
$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&#39;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 *&gt;$null
switch ($LASTEXITCODE) {
0 { &lt;# OK #&gt; }
1116 { &lt;# Shutdown has unexpectedly already been canceled, but that is a benign condition #&gt; }
default { Write-Host &quot;`nUNEXPECTED ERROR trying to cancel the pending shutdown.&quot;; exit $_ }
}

}

} while ($reschedule)

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

exit


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

确定