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))
Related
I am doing some automated uninstalls of Autodesk software, and Autodesk has once again screwed the pooch with their uninstalls. Their uninstall is supposed to do reference counting on certain shared components, like their Single Signor Service, Autodesk Genuine Service, Licensing service, etc. The problem is, when you are uninstalling that last ADSK product, the uninstaller is too stupid to stop the service, so their uninstaller fails with a 1603 fatal error. Last year you could stop the services before you started the uninstall, but this year I am getting this error
Stop-Service : Service 'Autodesk Access Service Host (Autodesk Access Service Host)' cannot be stopped due to the following error: Cannot open Autodesk Access Service Host service on computer '.'.
When using this code
Get-Service -Name "Autodesk Access Service Host" | Stop-Service -Force -NoWait
I have verified with
(Get-Service -Name "Autodesk Access Service Host").CanStop
that service can be stopped. At least according to the property.
I also tried
Start-Process "$env:WINDIR\system32\sc.exe" \\.,stop,"Autodesk Access Service Host" -NoNewWindow -Wait
while ((Get-Service -ComputerName '.' -Name "Autodesk Access Service Host" |
Select -ExpandProperty Status) -ne 'Stopped') {
Write-Host "Waiting for service to stop..."
Start-Sleep -Seconds 10
}
And that has run for 15 minutes with no results. Interestingly I CAN disable the service, but I really don't want that. I just want to stop it temporarily, so IF the Autodesk uninstall that is running is the last one with a dependency on this service will uninstall it correctly and returns the correct exit code of 0.
EDIT: I tried
sc stop "Autodesk Access Service Host"
from an elevated command prompt and that shows
STATE : 3 STOP_PENDING
(NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
so not really sure how to take STOP_PENDING along with NOT_STOPPABLE, nor why this would say NOT_STOPPABLE when the property above shows true.
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
I see some questions about this topic, but I cannot get it working
Get-Service -Name Spooler -ComputerName (Get-Content c:\tmp\scripts\Servers\iservers.txt) |
Stop-Service -PassThru | Set-Service -StartupType Disabled -whatif
The code executes for each server on the txt file, and stops de service, but not disable the service.
Any help to get it work and/or Troubleshooting???
Regards.
How to approach this kind of problem
In automation, we work up to complexity, meaning you should start simply and then add on more features until you see where it breaks.
Right now, you're trying to do a bunch of operations in one single line:
Load a list of computers and
Reach out to the computers and Stop a service and
Also while doing this, set the service to not automatically start.
There are a lot of problems you can run into, like "what happens if these PCs aren't enabled for remoting", or "what if you need a different account to handle stopping or disabling a service".
When you're trying to figure it all out in one-line, you're in for a bad and frustrating time.
How to fix it
Start simply. Start with one computer that's nearby and definitely turned on.
Begin with reading a service. Can you even get this operation to run?
Get-Service -ComputerName SomePC123 Spooler
Status Name DisplayName
------ ---- -----------
Running spooler Print Spooler
If you run into an error, then first figure out how to be able to remote into that one PC and see if the Print Spooler is running. Then, you will know what steps to deploy to all of your machines to prepare them for remoting.
Then, once you can check if a service is running, you can add on the next step, try to stop the service.
So your code would start to look like this:
$computers = get-content .\someTextFile.txt
forEach($computer in $computers){
$service = Get-Service -ComputerName $computer Spooler
"status of spooler on $computer is $($service.Status), with start type of $($service.StartType)"
#todo, set start type to Disabled...
}
Eventually, you will have migrated each step out of the one-liner and you'll know where and why any given command is failing. This is the way.
Update - the original question claimed that I was able to successfully perform an Invoke-Command and then shortly after was unable to; I thought it was due to processes going on during login after a windows upgrade.
It turns out the PC was actually starting, running a quick batch/cmd file, and then restarting. This is what was leading to being able to do PS Remoting and then suddenly not. The restart was quick enough after first boot that I didn't realize it was happening. Sorry for the bad question.
For the curious, the machine was restarting because of a remnant of the Microsoft Deployment Toolkit in-place upgrade process. The way MDT completes its task-sequence post-upgrade is problematic for many reasons, and now I've got another to count.
Old details (no longer relevant, with incorrect assumption that machine was not restarting after first successful Invoke-Command):
I'm automating various things with VMs in Hyper-V using powershell and powershell remoting. I'll start up a VM and then want to run some commands on it via powershell.
I'm struggling with determining when I can safely start running the remote commands via things like Invoke-Command. I can't start immediately as I need to let the machine start up.
Right now I poll the VM with a one second sleep between calls until the following function returns $true:
function VMIsReady {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)][object]$VM
)
$heartbeat = $vm.Heartbeat
Write-Host "vm heartbeat is $heartbeat"
if (($heartbeat -eq 'OkApplicationsHealthy') -or ($heartbeat -eq 'OkApplicationsUnknown'))
{
try
{
Invoke-Command -VMName $vm.Name -Credential $(GetVMCredentials) {$env:computername} | out-null
}
catch [System.Management.Automation.RuntimeException]
{
Write-Host 'Caught expected automation runtime exception'
return $false
}
Write-Host 'remoting ready'
return $true
}
}
This usually works well; however, after a windows upgrade has happened, there are issues. I'll get Hyper-V remoting errors of various sorts even after VMIsReady returns $true.
These errors are happening while the VM is in the process of first user login after upgrade (Windows going through "Hi;We've got some updates for your PC;This might take several minutes-Don't turn off your PC). VMIsReady returns true right as this sequence starts - I imagine I probably should be waiting until the sequence is done, but I've no idea how to know when that is.
Is there a better way of determining when the machine is in a state where I can expect remoting to work without issue? Perhaps a way to tell when a user is fully logged on?
You can use Test-WSMan.
Of run a script on the invoke that will receive a response from the server.
[bool]$Response | Out-Null
try{
$Response = Invoke-Command -ComputerName Test-Computer -ScriptBlock {return $true}
}catch{
return $false
}
if ($Response -ne $true){
return $false
}else{
return $true
}
I have three servers, let's call them Deploy1, Deploy2, Target.
All servers are running Windows Server 2008R2, fully updated.
A domain user, admin1, is configured as administrator on all servers, and this is the user I'm running all the commands with.
The following command works on Deploy1:
Get-Service "MyService" -ComputerName Target | Stop-Service
When running the same command on Deploy2, the command fails with the following message:
Cannot find any service with service name 'MyService'.
On Deploy2, the following command works, and displays the service and its status.
Get-Service "MyService" -ComputerName Target
Now, I know there are other ways to stop/start services via PowerShell, but I like this one as it automatically waits for the server to actually stop/start.
So what could be wrong with Deploy2?
Powershell v2.0 has a bug (feature?) in how the object returned by Get-Service is implemented. It does not actually set the ComputerName property correctly. Because of this, it can only affect local services. If you upgrade to Windows Management Framework 3.0 (and consequently Powershell v3) the bug is fixed and will work correctly.
Does this work? If not, is there an error produced?
(Get-Service "MyService" -ComputerName Target).Stop()