Powershell - CloseMainWindow work for all? - powershell

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

Related

How to get the proper PID of a newly created Firefox window in Powershell?

Here is a very simple example of the problem I am experiencing:
$process = Start-Process 'C:\Program Files\Mozilla Firefox\firefox.exe' -argumentlist "-new-window https://google.com -foreground" -PassThru
Write-Host $process.Id
The firefox window will start and work as expected, it will return a process id, but when I actually check the running processes, I see no results for that PID.
I tried adding this just to see,
while (-not $process.HasExited){
Write-Host "..."
}
Write-Host $process.HasExited
And it looks like the process does run for maybe a couple milliseconds before it exits.
I'm thinking this may have something to do with how Firefox handles it's own processes. Because I tested a similar setup with some other random apps and they all worked as expected.
Any ideas on how to work around this when it comes to Firefox?
There are several challenges to overcome:
The firefox process that ends up presenting the actual browser window is different from the one that is initially launched. That is, as you've observed, the launched process spawns other processes and itself exits quickly.
As Olaf points out, modern browsers typically launch multiple non-transient processes, so the challenge is how to identify the one that represent the browser window.
Browsers may reuse existing processes, so a single process can present multiple windows / tabs, and closing one of them won't terminate the process as a whole.
If you need to ensure that a dedicated, new process is used, you have two options:
(a) Make sure that no preexisting Firefox instance is running, either by erroring out, or - if acceptable for your use case - by forcefully terminating all existing instances first (Stop-Process -Name firefox).
(b) With significantly more effort, create a dedicated, temporary Firefox profile that you can launch with the -new-instance option, which allows multiple independent Firefox instances to run concurrently and whose lifetime can be tracked separately.
The following - cumbersome - solution implements option (b):
If no firefox process is found, there is no concern about creating independent instances, and Firefox can be launched normally.
Otherwise, a temporary profile is created, and launched via the -new-instance and -profile options to ensure that a new process will be used to present the new browser window.
After launching the initial process, loop until a firefox process appears that was launched later and has a nonempty window title, which is then presumed to be the real process of interest.
You can then wait for the termination of this process to know when the dedicated browser window has been closed. If a temporary profile had to be created, it is cleaned up afterwards.
# Comment this statement out to silence the verbose messages below.
$VerbosePreference = 'Continue'
$now = Get-Date
$url = 'https://example.org' # URL to open.
# Launch a (new) Firefox instance.
if ($alreadyRunning = [bool] (Get-Process -ErrorAction Ignore firefox)) {
# Determine the path for a temporary profile with a unique name.
$tempProfilePath = Join-Path ([IO.Path]::GetTempPath()) ([datetime]::utcnow.tostring('o') -replace '\D')
Write-Verbose "Firefox is already running. Creating temp. profile $tempProfilePath..."
# Note: Creating an empty directory for the profile is seemingly enough.
$null = New-Item -Type Directory $tempProfilePath
Write-Verbose "and starting a new instance with it..."
Start-Process firefox "-new-instance -profile $tempProfilePath $url"
} else {
Write-Verbose 'Firefox isn''t running. Starting normally...'
Start-Process firefox $url
}
# Find the newly launched process that is the actual browser window.
Write-Verbose 'Waiting for a recently launched Firefox process with a nonempty window title to appear...'
while (-not (
$ps = Get-Process firefox |
Where-Object StartTime -gt $now |
Where-Object MainWindowTitle
)) {
Write-Host -NoNewLine .
Start-Sleep -MilliSeconds 500
}
Write-Host
Write-Verbose "Found. Waiting for process to exit..."
$ps.WaitForExit()
Write-Verbose 'Process has exited.'
if ($alreadyRunning) {
Write-Verbose "Cleaning up temporary profile $tempProfilePath..."
do {
# The profile dir. is typically held on to for a little while longer by associated processes that may not have terminated yet.
Start-Sleep -MilliSeconds 200
Remove-Item -ErrorAction Ignore -Literalpath $tempProfilePath -Recurse -Force
}
while (Test-Path -LiteralPath $tempProfilePath)
}
Thanks to #mklement0 work, in your case you can use the parent Process ID.
I use WMI to get the parent process, but it works for the very first launch.
$parentProcess = Start-Process 'C:\Program Files\Mozilla Firefox\firefox.exe' -argumentlist "-new-window https://google.com -foreground" -PassThru
$childProcess = get-process -id $(Get-CimInstance -Class Win32_Process -Filter "Name = 'firefox.exe'" | where {$_.ParentProcessId -eq $parentProcess.id}).ProcessId
# effectively stop the child
$childProcess | Stop-Process

Powershell AcceptTcpClient() cannot be interrupted by Ctrl-C

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:

How to get working set memory from a SYSTEM process

I am fairly unexperienced in powershell and I need to pull the memory usage from a process that falls under the SYSTEM user while logged in under my user in Windows Server 2012.
I can pull the information from my user easily but there is a large discrepancy from that and what it shows in task manager under the SYSTEM user.
The goal is to stop the java.exe process if it is using a certain amount of memory as shown below.
$java = Get-Process “java” -ErrorAction SilentlyContinue
if (!$java.WS/1GB -gt 20) {
$java | Stop-Process -Force}
However, when I run
(Get-Process "java").WS/1GB
I get ~1.364GB when the process under the system user in task manager is using ~173GB.

Whats the difference between powershell's Stop-Service and NET-STOP

In powershell, I've seen multiple ways to stop a service
The more modern way
Stop-Service wuauserv
And the more legacy way
NET STOP WUAUSERV
The legacy way is much more difficult to automate because it is not natively idempotent.
I have powershell scripts that builds a windows golden images using packer. Initially the scripts used NET STOP. I found once I switched to Stop-Service, I seemed to get more frequent failures when rebooting a VM after installing windows updates.
Do both Stop-Service and NET STOP produce the same result? Or are there differences between them that might explain why the legacy one seems more reliable?
For a Windows service that is:
currently running
and stoppable in principle
both net stop and Stop-Service should act the same, namely synchronously:
That is, they send the specified service a stop request and wait for stopping to complete (net stop invariably waits, while, in PSv5+, you can opt out of waiting with Stop-Service's -NoWait switch).
Unlike net stop (which reports an error if the service is already stopped), Stop-Service is idempotent (exhibits desired-state logic): If the target service is already in the stopped state, the command is a quiet no-op.
(As an aside: Start-Service is also synchronous, but invariably so, and is also idempotent.)
Set-Service -Status Stopped should act the same as Stop-Service, except that:
unlike Stop-Service, it doesn't support -Force in order to stop a service with running dependents (other services that depend on the service being stopped).
due to what I presume to be a bug you cannot even stop services that themselves depend on other services(!).
in effect, as of Windows PowerShell v5.1 / PowerShell Core v6.0-rc, you can only stop services with Set-Service -Status Stopped that have no dependents (no services that depend on them), nor themselves depend on other services.
Optional reading: looking at the Stop-Service and Start-Service source code:
The publicly available source code on GitHub is for the cross-platform Core edition of PowerShell, but it looks like the code in question was essentially taken unmodified from the Windows PowerShell version.
Stop-Service: The part where the code waits for the service to be in the ServiceControllerStatus.Stopped state[1]
, which is only bypassed if the -NoWait switch is explicitly specified, in which case variable waitForServiceToStop is set to false.
Start-Service: The part where the code invariably waits for the service to be in the ServiceControllerStatus.Running state.
[1] If reaching the target state takes longer than 2 seconds, the waiting loop issues a warning (every 2 seconds) while continuing to wait; waiting is only aborted with an error if the service is unexpectedly neither in the target-state-pending state nor in the target state.
The difference is waiting until the service is actually stopped. net stop service waits until service gets stopped, or at least sends the event that it's now "stopped". The "other legacy" way of sc stop service exits at once after sending stop signal, and dumps current service state which is normally STOP_PENDING. Stop-Service cmdlet does wait for service to stop, but there might be corner cases with services that are stopping for too long and the cmdlet bails off, or it had a -nowait switch in there. Also some services are restarted if needed, so a further check might be required, like this (in case a service didn't stop):
Stop-Service $servicename
$sleep=0
$s="Running"
do {
$sleep++
start-sleep 1
$s=(get-service $servicename).status
} while (($s -ne "Stopped") -and ($sleep -le 20))

Getting Processes via Get-Counter does not refresh when processes change and throws errors if processes end

My script monitors the CPU usage of the process, looping the code every 5 sec and writing it to a file. Which works fine.
But I found when a new Process runs my script will not find it until I stop the script and rerun it again.
Also if a process ends/stops, the script give an this error:
Get-Counter : The data in one of the performance counter samples is
not valid. View the Status property for each
PerformanceCounterSample object to make sure it contains valid data.
At line:2 char:34
It seems PowerShell retrieves the Process information only once and caches it.
If I run the bellow script (which is a part of all my script), it runs perfectly:
while($true) {
$ProcessId = (Get-Counter "\Process(*)\ID Process").CounterSamples
$ProcessId.count
Start-Sleep -s 5
}
If I have 50 process it will gives 50, but if a new process starts it will keep giving 50 until I restart the script.
If I stop any process it will give the same error above.
Any idea how to solve this problem and force PowerShell to reread the process list without restarting the script?
You could use PowerShell Jobs to execute it in a new background process on each iteration and use -ErrorAction SilentlyContinue to suppress the error messages that might occur if one or more processes stopped during a check:
while($true) {
$ProcessId = Start-Job -ScriptBlock { (Get-Counter "\Process(*)\ID Process" -ErrorAction SilentlyContinue).CounterSamples } | Wait-Job | Receive-Job
$ProcessId.Count
Start-Sleep -Seconds 5
}