-->

Powershell AcceptTcpClient() cannot be interrupted

2020-07-26 11:21发布

问题:

I am writing a simple TCP/IP server using Powershell. I notice that Ctrl-C cannot interrupt the AcceptTcpClient() call. Ctrl-C works fine after the call though. I have searched around, nobody reported similar problem so far.

The problem can be repeated by the following simple code. I am using Windows 10, latest patch, with the native Powershell terminal, not Powershell ISE.

$listener=new-object System.Net.Sockets.TcpListener([system.net.ipaddress]::any, 4444)
$listener.start()
write-host "listener started at port 4444"
$tcpConnection = $listener.AcceptTcpClient()
write-host "accepted a client"

This is what happens when I run it

ps1> .\test_ctrl_c.ps1
listener started at port 4444
(Ctrl-C doesn't work here)

回答1:

(As of PowerShell 7.0) Ctrl-C only works while PowerShell code is executing, not during execution of a .NET method.

Since most .NET method calls execute quickly, the problem doesn't usually surface.

See this GitHub issue for a discussion and background information.


As for possible workarounds:

  • The best approach - if possible - is the one shown in your own answer:

    • Run in a loop that periodically polls for a condition, sleeping between tries, and only invoke the method when the condition being met implies that the method will then execute quickly instead of blocking indefinitely.
  • If this is not an option (if there is no such condition you can test for), you can run the blocking method in a background job, so that it runs in a child process that can be terminated on demand by the caller; do note the limitations of this approach, however:

    • Background jobs are slow and resource-intensive, due to needing to run a new PowerShell instance in a hidden child process.

    • Since cross-process marshaling of inputs to and outputs from the job is necessary:

      • Inputs and output won't be live objects.
      • Complex objects (objects other than instances of primitive .NET types and a few well-known types) will be emulations of the original objects; in essence, objects with static copies of the property values, and no methods - see this answer for background information.

Here's a simple demonstration:

# Start the long-running, blocking operation in a background job (child process).
$jb = Start-Job -ErrorAction Stop {
  # Simulate a long-running, blocking .NET method call.
  [Threading.Thread]::Sleep(5000)
  'Done.'
}

$completed = $false
try {

  Write-Host -ForegroundColor Yellow "Waiting for background job to finish. Press Ctrl-C to abort."

  # Note: The output collected won't be *live* objects, and with complex
  #       objects will be *emulations* of the original objects that have
  #       static copies of their property values and no methods.
  $output = Receive-Job -Wait -Job $jb

  $completed = $true

}
finally { # This block is called even when Ctrl-C has been pressed.

  if (-not $completed) { Write-Warning 'Aborting due to Ctrl-C.' }

  # Remove the background job.
  #  * If it is still running and we got here due to Ctrl-C, -Force is needed
  #    to forcefully terminate it.
  #  * Otherwise, normal job cleanup is performed.
  Remove-Job -Force $jb

  # If we got here due to Ctrl-C, execution stops here.
}

# Getting here means: Ctrl-C was *not* pressed.

# Show the output received from the job.
Write-Host -ForegroundColor Yellow "Job output received:"
$output
  • If you execute the above script and do not press Ctrl-C, you'll see:

  • If you do press Ctrl-C, you'll see:



回答2:

After getting @mklement0's answer, I gave up my original clean code. I figured out a workaround. Now Ctrl-C can interrupt my program

$listener=new-object System.Net.Sockets.TcpListener([system.net.ipaddress]::any, 4444)
$listener.start()
write-host "listener started at port 4444"
while ($true) {
   if ($listener.Pending()) {
      $tcpConnection = $listener.AcceptTcpClient()
      break;
   }
   start-sleep -Milliseconds 1000
}
write-host "accepted a client"

Now Ctrl-C works

ps1> .\test_ctrl_c.ps1
listener started at port 4444
(Ctrl-C works here)