Powershell AcceptTcpClient() cannot be interrupted by Ctrl-C - powershell

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)

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)

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

Related

Powershell - CloseMainWindow work for all?

I'm looking for a way to gracefully close/quit the GoogleDrive app which runs under the process GoogleDriveFS.
get-process GoogleDriveFS
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
219 16 10796 5732 0.05 4392 1 GoogleDriveFS
333 22 11820 32364 0.17 8424 1 GoogleDriveFS
297 19 16528 34860 0.06 12036 1 GoogleDriveFS
245 17 10472 23992 0.03 14296 1 GoogleDriveFS
572 26 52256 82728 0.84 17788 1 GoogleDriveFS
518 21 28668 68208 0.44 18460 1 GoogleDriveFS
1024 59 47016 89396 27.95 19452 1 GoogleDriveFS
is something like Process.CloseMainWindow Method suitable for this ? or is there a better way to ensure the app isn't running?
tl;dr
System.Diagnostics.Process.CloseMainWindow() will not work, for the reasons explained in the bottom section.
Note:
If the target processes weren't started from your own user account, you'll need to run the following from an elevated (run as admin) session.
You can try the following to achieve graceful termination, but there's no guarantee it will work:
# Asks all GoogleDriveFS processes to terminate, which they may or may not do.
# A status line is output to stdout for each targeted process,
# indicating whether the termination request was successfully *sent*.
# Note: ".exe" must be used, whereas it mustn't be
# with PowerShell's *-Process cmdlets.
taskkill.exe /im GoogleDriveFS.exe
If it doesn't, forceful termination is your only option, which is most easily accomplished with:
# !! Forcefully terminates all GoogleDriveFS, without cleanup.
Stop-Process -Force -Name GoogleDriveFS
Note: As discussed below, Stop-Process always terminates forcefully. The only function of the -Force switch is to suppress a potential confirmation prompt that is presented when you attempt to terminate processes belonging to a different user (only works with elevation).
Here's a snippet that first tries graceful termination, then falls back to forceful termination after a specifiable timeout:
$processName = 'GoogleDriveFS'
$timeOutSecs = 2
# Get all existing processes of interest.
$processes = Get-Process -ErrorAction Ignore -Name $processName
if (-not $processes) {
Write-Verbose -Verbose "No $processName processes running."
} else {
# Ask the processes to terminate, which they may or may not do.
taskkill.exe /im "$processName.exe" *>$null
try {
# Wait for up to $timeOutSecs seconds for the processes to -
# potentially - terminate gracefully.
$processes | Wait-Process -ErrorAction Stop -Timeout $timeOutSecs
} catch {
Write-Warning "Forcefully terminating (remaining) $processName processes..."
# Note: This assumes that you don't care about any new
# processes that may have launched since Get-Process was called.
$processes | Stop-Process -Force
}
}
On Windows, graceful termination is fundamentally only an option for GUI-subsystem applications, i.e. processes that have a main window (whether visible or not) and therefore a message loop to which the WM_CLOSE message can be posted.
In other words: you cannot ask console applications on Windows to terminate gracefully (unless they implement some application-specific custom mechanism through which other processes can request termination).
For supported applications, there are important considerations:
Termination isn't guaranteed, and even if it does happen, its timing isn't guaranteed:
The target process may be in a state where it cannot process the WM_CLOSE message, such as when it happens to be displaying a modal dialog at the time or happens to be stuck.
The target process may quietly refuse to terminate.
The target process may put up a modal dialog to confirm the intent to terminate, notably when trying to close an editor-like application that has an unsaved document open.
Therefore, if you need to ensure termination, you'll have to monitor the process for actual termination afterwards, and possibly terminate it forcefully after a suitable timeout period.
taskkill.exe offers forceful termination via its /f option.
.NET offers forceful termination via System.Diagnostics.Process.Kill()
As an aside: As of PowerShell 7.2.x, the Stop-Process cmdlet invariably uses this method, i.e. invariably terminates processes forcefully - allowing requesting graceful termination on an opt-in basis is the subject of GitHub issue #13664.
At the Windows API level, it doesn't matter if the targeted main window is visible or not, so that even (GUI-subsystem) processes that by design run invisibly - as GoogleDriveFS.exe appears to be - can be targeted with a WM_CLOSE message.
While System.Diagnostics.Process.CloseMainWindow() is designed to request graceful termination of a given process by sending a WM_CLOSE message to its main window, it unfortunately doesn't find that window if it happens to be invisible (hidden) (still applies as of .NET 6.0)
By contrast, the taskkill.exe utility does not have this limitation.
A limitation that BOTH methods share is the inability to target processes that are UWP / Microsoft Store applications.
However, this applies only to "pure" UWP applications (e.g, Settings, Calculator), and not to desktop applications packaged as UWP apps (e.g., Windows Terminal, Microsoft Edge).
The reason is that both methods rely on the EnumWindows WinAPI method, which only supports desktop applications.
However, manually finding a UWP application's main window via FindWindowEx and posting WM_CLOSE to it, is possible.
You can do something like this:
do {
$running = try { Get-Process -Name GoogleDriveFS -ErrorAction Stop } catch { Write-Host "Error: $($PSItem.Exception.Message) " }
$running | ForEach-Object {
$_.CloseMainWindow()
Write-Debug -Message "Closing ($_).pm "
}
}
until ($running -eq $null)
However, this will prompt for the user to confirm the close.
You could also use the close() method to avoid prompting the user or kill() to immediately release all resources.
CloseMainWindow()
Closes a process that has a user interface by sending a close message to its main window.
Close() Frees all the resources that are associated with this component.
Kill() Immediately stops the associated process.
via https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process?view=net-6.0#methods
You could use the --quit argument of GoogleDriveFS.exe :
"C:\Program Files\Google\Drive File Stream\64.0.4.0\GoogleDriveFS.exe" --quit
But it will break after each software update so running this bat file should be betterĀ :
"%ProgramFiles%\Google\Drive File Stream\launch.bat" --quit
This bat file looks up the latest GoogleDriveFS.exe and runs it with the same arguments as the script.
And from Powershell :
Start-Process -FilePath "${env:ProgramFiles}\Google\Drive File Stream\launch.bat" -ArgumentList '--quit' -Wait -NoNewWindow

Powershell Script cycles through machines but hangs if one loses network temporarily

I have a powershell script that parses a txt file which is full of machine names, then one by one, it creates a session to the system, runs a few commands, and moves to the next system. The script usually take about 10-30 seconds to run on each system depending on the case encountered in the script.
Once in a while the system that is currently being checked will lose the network connection for some various reason. When this happens the console starts writing yellow warning messages about attempting to reconnect for 4 minutes and then disconnects the session when it cannot reconnect.
Even if it establishes the connection again within the 4 minutes, it doesn't do anything after that, it's like the script just freezes. It won't move on to the next system and it doesn't stop the script, I have to manually stop it, or if i manually run the script, i can hit control+c to break out of the current loop, and it then moves on to the next machine in the list.
Is there any way to break out of the current loop if a warning is encountered so it can move on to the next machine? That would be my ideal solution. thanks!
Script is simple..
foreach($server in Get-Content .\machines.txt) {
if($server -match $regex){
invoke-command $server -ErrorAction SilentlyContinue -ScriptBlock{
command1
command2
command3
}
}
this is what happens
PS C:\temp> .\script.ps1
machine1
machine2
machine3
machine4
machine5
WARNING: The network connection to machine5 has been interrupted. Attempting to reconnect for up to 4 minutes...
WARNING: Attempting to reconnect to machine5 ...
WARNING: Attempting to reconnect to machine5 ...
WARNING: Attempting to reconnect to machine5 ...
WARNING: The network connection to machine5 has been restored.
But it never goes on to machine6
When i work remotely with multiple machines i usually start the processes on the machines in parallel. So i have less impact when single machines are timing out. I use powershell 7 ForEach-Object -Parallel Feature for this https://devblogs.microsoft.com/powershell/powershell-foreach-object-parallel-
feature/
Try something like this:
$Credential=Get-Credential
#all Necessary parameters must be in the Object i give to ForEach Object
$myHosts = #(
#Hosts i want to connect to with values i want to use in the loop
#{Name="probook";cred=$Credential;param1="one_1";param2="two_1"}
#{Name="probook";cred=$Credential;param1="one_2";param2="two_2"}
)
$var1="one"
$var2="two"
$myHosts | ForEach-Object -Parallel {
#Variables outside of this "Parallel" Loop are not available. Because this is startet as separate SubProcess
#All Values come from the Object i piped in the ForEach-Object
$myHost=$_
#This is written to your local Shell
Write-Host ("Computer: "+ $env:Computername)
Write-Host $myHost.param1
Write-Host $myHost.param2
Write-Host $myHost.cred.UserName
Invoke-Command -ComputerName $myHost.Name -Credential $myHost.cred -ArgumentList #($myHost.param1,$myHost.param2) -ScriptBlock {
#Variables outside of of this Invoke Command Script Block are not available because this is a new Remote-Shell on the remote Host
#Parameters in Ordner of -Argument List
param($param1,$param2)
#Do your things on the Remote-Host here
#This is not Visbible -> it is only written on the "remote Shell"
Write-Host $env:Computername
#Here you get Back Values from the remote Shell
$env:Computername
$param1
$param2
}
} -ThrottleLimit 5
Hmm his is indeed a Problem.
You could experiment with:
Start-Job
(https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/start-job?view=powershell-7.1)
Get-Job (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-job?view=powershell-7.1)
Receive-Job (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/receive-job?view=powershell-7.1)
So you have more control what the processes do.
You start Background Jobs with Start-Job. Start-Job deliveres Job Objects Back -> save them in a array or variables
With Get-Job you see all Jobs currently Running
with Receive-Job you get back the output from a Job so far. You can use receive-Job to get back all PSObjects a Background Job has written.
Cannot explain in Detail, but this woul be another try i would do for this job.

Powershell - Loop run console app >Wait for Ctrl+c

I have a windows console app that currently runs to process some files, at the end of the run, if successful, it starts a windows service and I get the output > xxx service is now running, press control_c to exit.
The console app looks at a config file to pull some parameters, I need to be able to re-run this multiple times while changing the parameters in the config file first. To do this manually I'd do the following:
change config file
run the app from powershell
wait for the message above to appear
click ctrl + c to terminate
change config file and run again
I thought it makes sense to automate this in a PS script where I can just pass the config values for all the runs, then the script loops through the values, edit the config file and run the exe.
Issue I have is the loop gets "stuck" at first run because the application is waiting for the ctrl+c command so never progresses through the loop.
what I have at the moment looks like this:
foreach ($dt in $datesarr)
{
##edit config values with stuff in $dt
$output=(<path to app here>)
while ($output[-1] -notlike "*Control-C*")
{
Start-Sleep -Seconds 10
}
}
problem I have is the script never reaches the while loop as it's just stuck after running the app awaiting for ctrl + c... What I want it to do is launch the app, wait for it to get to the ctrl + c bit then exit the loop and pick the second value in the parameter.
Any thoughts would be hugely appreciated!
Try the following approach, which is based on direct use of the following, closely related .NET APIs:
System.Diagnostics.ProcessStartInfo
System.Diagnostics.Process
Instead of trying to programmatically press Ctrl-C, the process associated with the external program is simply killed (terminated).
# Remove the next line if you don't want to see verbose output.
$VerbosePreference = 'Continue'
$psi = [System.Diagnostics.ProcessStartInfo] #{
UseShellExecute = $false
WindowStyle = 'Hidden'
FileName = '<path to app here>'
Arguments = '<arguments string here>' # only if args must be passed
RedirectStandardOutput = $true
RedirectStandardError = $true # optional - if you also want to examine stderr
}
Write-Verbose "Launching $($psi.FileName)..."
$ps = [System.Diagnostics.Process]::Start($psi)
Write-Verbose "Waiting for launched process $($ps.Id) to output the line of interest..."
$found = $false
while (
-not $ps.HasExited -and
-not ($found = ($line = $ps.StandardOutput.ReadLine()) -match 'Control-C')
) {
Write-Verbose "Stdout line received: $line"
}
if ($found) {
Write-Verbose "Line of interest received; terminating process $($ps.Id)."
# Note: If the process has already terminated, this will be a no-op.
# .Kill() kills only the target process itself.
# In .NET Core / .NET 5+, you can use .Kill($true) to also
# kill descendants of the process, i.e. all processes launched
# by it, directly and via its children.
$ps.Kill()
} else {
Write-Error "Process $($ps.Id) terminated before producing the expected output."
}
$ps.Dispose()

Stop a process running longer than an hour

I posted a question a couple ago, I needed a powershell script that would start a service if it was stopped, stop the process if running longer than an hour then start it again, and if running less than an hour do nothing. I was given a great script that really helped, but I'm trying to convert it to a "process". I have the following code (below) but am getting the following error
Error
"cmdlet Start-Process at command pipeline position 3
Supply values for the following parameters:
FilePath: "
Powershell
# for debugging
$PSDefaultParameterValues['*Process:Verbose'] = $true
$str = Get-Process -Name "Chrome"
if ($str.Status -eq 'stopped') {
$str | Start-Process
} elseif ($str.StartTime -lt (Get-Date).AddHours(-1)) {
$str | Stop-Process -PassThru | Start-Process
} else {
'Chrome is running and StartTime is within the past hour!'
}
# other logic goes here
Your $str is storing a list of all processes with the name "Chrome", so I imagine you want a single process. You'll need to specify an ID in Get-Process or use $str[0] to single out a specific process in the list.
When you store a single process in $str, if you try to print your $str.Status, you'll see that it would output nothing, because Status isn't a property of a process. A process is either running or it doesn't exist. That said, you may want to have your logic instead check if it can find the process and then start the process if it can't, in which case it needs the path to the executable to start the process. More info with examples can be found here: https://technet.microsoft.com/en-us/library/41a7e43c-9bb3-4dc2-8b0c-f6c32962e72c?f=255&MSPPError=-2147217396
If you're using Powershell ISE, try storing the process in a variable in the terminal, type the variable with a dot afterwards, and Intellisense (if it's on) should give a list of all its available properties.

InvocationStateChanged Event Raised Twice in PowerShell Script

I have been writing a script in PowerShell V4 on Windows 8.1 which makes use of background processes and events. I have found something which is a little strange. Rather than post the 2,500 lines or so of my script I have included a much shorter program which exhibits the odd behaviour. I expect it is something I am doing wrong but I cannot see what the problem is. The code is as follows:
`
# Scriptblocks to simulate the background task and the action to
# take when an event is raised
[scriptblock] $MyScript = {
for ($i = 0;$i -lt 30;$i++)
{
[console]::writeline("This is a test - $i")
start-sleep -m 500
}
}
[scriptblock] $StateChanged = {
[console]::writeline("The state changed")
}
# Create a runspace pool
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, [int] $env:NUMBER_OF_PROCESSORS + 1)
$RunspacePool.ApartmentState = "MTA"
$RunspacePool.Open()
# Create and start the background task to run
$PS = [powershell]::Create()
[void] $PS.AddScript($MyScript)
$PS.RunspacePool = $RunspacePool
$Asyncresult = $PS.BeginInvoke()
# Register an interest in the InvocationStateChanged event for
# the background task. Should the event happen (which it will)
# run the $StateChanged scriptblock
Register-ObjectEvent -InputObject $PS -EventName InvocationStateChanged -Action $StateChanged
# The loop that simulates the main purpose of the script
[int] $j = 0
while ($PS.InvocationStateInfo.State -eq [System.Management.Automation.PSInvocationState]::Running)
{
if ($j -eq 2)
{
[void] $PS.BeginStop($NULL, $NULL)
}
"Running: $j" | out-host
sleep -m 400
$j = $j + 1
}
sleep 10
`
Essentially all it does is create a runspace to run a powershell scriptblock and while that is running something else happens in the foreground. I simulate someone pressing a button or, for whatever reason, a beginstop method being executed to stop the background process. That all works and the background process duly stops. However, I have registered an event for the background powershell script which runs a scriptblock when the background job changes state. The strange thing is that the scriptblock gets invoked twice and I cannot work out why.
Here is some output from running the script:
E:\Test Programs>powershell -file .\strange.ps1
This is a test - 0
Running: 0
This is a test - 1
Running: 1
The state changed
Running: 2
The state changed
E:\Test Programs>
As you can see it displays "The state changed" twice. They are a fraction of a second apart. I put a sleep 10 at the end to eliminate the possibility that it is the script stopping that is causing the second "The state changed" message.
If anyone can explain what is wrong I would be very grateful.
The InvocationStateChanged event is likely being called when on Running and Completed
If you change $StateChanged to also include the $Sender.InvocationStateInfo.State automatic variable properties, like this:
[scriptblock] $StateChanged = {
[console]::writeline("The state changed to: $($Sender.InvocationStateInfo.State)")
}
Your output will probably look like:
This is a test - 0
Running: 0
This is a test - 1
Running: 1
The state changed to: Running
Running: 2
The state changed: Completed
I am very sorry that I didn't reply a year and a half ago as I should have done. For some reason I never had a notification that there was an answer and, to be honest, I just forgot to check.
Anyway, thanks for the answer. I still have my little test script although the thing I was actually writing has been dumped and rewritten but I did check with my little test script and what I get is:
Running: 0
Running: 1
This is a test - 1
The state changed to: Stopped
The state changed to: Stopped
Running: 2
This is now on Windows 10.
Oh well, thanks for the suggestion anyway. As I said I've rewritten the original script so this is now just for interest.
Best wishes........
Colin