Powershell Wait for service to be stopped or started - powershell

I have searched both this forum and through google and can't find what I need.
I have a quite large script and I'm looking for some code that will check if the service is started or stopped before proceeding to the next step.
The function it self need to loop untill it's either stopped or started (Going to have a function for Stopped and one for Started).
In total 4 services which almost have the same name, so Service Bus * can be used as a wildcard.

I couldn't get the 'count' strategy, that Micky posted, to work, so here is how i solved it:
I created a function, that takes a searchString (this could be "Service Bus *") and the status that i expect the services should reach.
function WaitUntilServices($searchString, $status)
{
# Get all services where DisplayName matches $searchString and loop through each of them.
foreach($service in (Get-Service -DisplayName $searchString))
{
# Wait for the service to reach the $status or a maximum of 30 seconds
$service.WaitForStatus($status, '00:00:30')
}
}
The function can now be called with
WaitUntilServices "Service Bus *" "Stopped"
or
WaitUntilServices "Service Bus *" "Running"
If the timeout period is reached, a not so graceful exception is thrown:
Exception calling "WaitForStatus" with "2" argument(s): "Time out has expired and the operation has not been completed."

In addition to the answer of mgarde this one liner might be useful if you just want to wait for a single service (also inspired by a post from Shay Levy):
(Get-Service SomeInterestingService).WaitForStatus('Running')

The following will loop and verify the status of the given services until the number of services with the "Running" state is equal to zero (hence they are stopped), so you can use this if you are waiting for services to Stop.
I've added a $MaxRepeat variable, which will prevent this from running for ever. It will run 20 times max as defined.
$services = "Service Bus *"
$maxRepeat = 20
$status = "Running" # change to Stopped if you want to wait for services to start
do
{
$count = (Get-Service $services | ? {$_.status -eq $status}).count
$maxRepeat--
sleep -Milliseconds 600
} until ($count -eq 0 -or $maxRepeat -eq 0)

I had to tweak this a bit with multiple counters because this service purposely starts and stops slowly. The original script got me on the right track. I had to wait for the service to be in a completely stopped status before I could move on because I'm actually restarting that same service.
You could probably remove the "sleep," but I don't mind leaving it in.
You could probably remove everything and just use the $stopped variable. :)
# change to Stopped if you want to wait for services to start
$running = "Running"
$stopPending = "StopPending"
$stopped = "Stopped"
do
{
$count1 = (Get-Service $service | ? {$_.status -eq $running}).count
sleep -Milliseconds 600
$count2 = (Get-Service $service | ? {$_.status -eq $stopPending}).count
sleep -Milliseconds 600
$count3 = (Get-Service $service | ? {$_.status -eq $stopped}).count
sleep -Milliseconds 600
} until ($count1 -eq 0 -and $count2 -eq 0 -and $count3 -eq 1)

In my Azure build/deployment pipelines I use it like this to start and stop services (after already having sent a 'Stop' command asynchronously before) and which works for all transitional states like Starting, Stopping, Pausing and Resuming (which are called StartPending, StopPending, PausePending and ContinuePending in the status enumeration ServiceControllerStatus).
# Wait for services to be stopped or stop them
$ServicesToStop | ForEach-Object {
$MyService = Get-Service -Name $_ -ComputerName $Server;
while ($MyService.Status.ToString().EndsWith('Pending')) {
Start-Sleep -Seconds 5;
$MyService.Refresh();
};
$MyService | Stop-Service -WarningAction:SilentlyContinue;
$MyService.Dispose();
};
This needs a traditional powershell to function on a remote server, the cmdlet of pwsh.exe does not include parameter -ComputerName.
In my opinion no counters are needed as only transitional states cause the cmdlet to fail and they change anyway to one of the supported states in the near future (maximum 125 seconds for a Stop command).

To add more details to my response to #Christoph
here is a script i recently created to stop services and ensure the processes are also stopped. in our case the processes were predictable. it may be necessary to do more work to get the service/processid mapping if you have multiple services running off the same executeable.
$MaxWait = 180 #seconds
$ServiceNames = "MyServiceName*"
$ProcName = 'MyServiceProcName' #for the services
$sw = [System.Diagnostics.Stopwatch]::StartNew() # to keep track of
$WaitTS = (New-TimeSpan -Seconds $MaxServiceWait) #could also use a smaller interval if you want more progress updates
$InitialServiceState = get-service $ServiceNames | select Name,Status,StartType
write-Host "$ENV:COMPUTERNAME Stopping $ServiceNames"
$sw.Restart()
$Services = #()
$Services += Get-Service $ServiceNames | where Status -EQ Running | Stop-Service -PassThru -NoWait #nowait requires powershell 5+
$Services += Get-Service $ServiceNames | where Status -Like *Pending
#make sure the processes are actually stopped!
while (Get-Process | where Name -Match $ProcName)
{
#if there were services still running
if ($Services) {
Write-Host "$ENV:COMPUTERNAME ...waiting up to $MaxServiceWait sec for $($Services.Name)"
#wait for the service to stop
$Services.WaitForStatus("Stopped",$WaitTS)
}
#if we've hit our maximum wait time
if ($sw.Elapsed.TotalSeconds -gt $MaxServiceWait) {
Write-Host "$ENV:COMPUTERNAME Waited long enough, killing processes!"
Get-Process | where name -Match $ProcName | Stop-Process -Force
}
Start-Sleep -Seconds 1
#get current service state and try and stop any that may still be running
#its possible that another process tried to start a service while we were waiting
$Services = #()
$Services += Get-Service $ServiceNames | where Status -EQ Running | Stop-Service -PassThru -NoWait #nowait requires powershell 5+
$Services += Get-Service $ServiceNames | where Status -Like *Pending
}

Related

In Windows Powershell, how can I wait for an event to be true before proceeding?

I'm writing a Windows Powershell script that stops a service, then I want to print the service's status when it's finally stopped. I've tried the following, but it just hangs
$service = Get-Service | Where-Object {$_.name -like "*MyService*"}
Stop-Service -Name $service.name
$wait=true
while ($wait) {
if($service.Status -eq "Running") {
Start-Sleep -Seconds 1
}
else {
$wait=$false
}
}
I know I can probably write a for{} loop instead that counts 0-9, and breaks when my condition is met, but is there a better way?
tanstaafl's helpful answer addresses your immediate problem:
The .Status property value of a [System.ServiceProcess.ServiceController] instance (as returned by Get-Service) is a static value that only reflects the status of the service at the time of the Get-Service call.[1]
To update the value to reflect the then-current status, call the .Refresh() method.
However, there is no need to explicitly wait for a service to stop, because Stop-Service is synchronous, i.e.:
It waits for the service to finish stopping before returning, unless you explicitly pass -NoWait.
If that doesn't happen within a fixed, 2-second timeout:[2]
A warning is issued if the service last reported that stopping is pending - potentially, stopping will eventually finish.
Otherwise, a non-terminating error occurs - this suggests that the service is stuck.
Thus, you can simplify your code as follows:
# Report a script-terminating error if stopping doesn't finish
# within the timeout period.
Stop-Service -Name *MyService* -ErrorAction Stop -WarningAction Stop
More work is needed if you want to implement a retry mechanism.
[1] There is one exception, although the behavior is undocumented and should be considered an implementation detail: If you pipe a preexisting ServiceController instance to Stop-Service / Start-Service, these cmdlets refresh the instance for you; e.g., after executing ($service = Get-Service Bits) | Stop-Service, $service.Status is current (reflects Stopped).
[2] As of PowerShell Core 7.3.0-preview.2 - see the source code.
You need to re-check your service within the loop.
$service = Get-Service | Where-Object {$_.name -like "*MyService*"}
Stop-Service -Name $service.name
$wait=true
while ($wait) {
if($service.Status -eq "Running") {
Start-Sleep -Seconds 1
# ADD THIS BELOW. Need to re-check service in loop.
$service = Get-Service | Where-Object {$_.name -like "*MyService*"}
}
else {
$wait=$false
}
}

To check a windows service state as running by running a loop upto 3 increments

Hi I have a scenario where I need to to check a windows service state as running by running a loop upto 3 increments and after every increment wait for 10 sec before incrementing the loop.How to achieve this?
Perhaps using a simple for() loop like this:
$serviceName = 'TheServiceName'
$maxTries = 3
for ($i = 0; $i -lt $maxTries; $i++) {
$status = (Get-Service -Name $serviceName).Status
Write-Host "Service '$serviceName' status is $status"
if ($status -eq 'Running') {
# exit the loop
break
}
# service is not running, so sleep for 10 seconds and try again
Start-Sleep -Seconds 10
}
Make sure you test on the service Name or DisplayName property, they differ..

Test-Path timeout for PowerShell

I'm trying to routinely check the presence of particular strings in text files on hundreds of computers on our domain.
foreach ($computer in $computers) {
$hostname = $computer.DNSHostName
if (Test-Connection $hostname -Count 2 -Quiet) {
$FilePath = "\\" + $hostname + "c$\SomeDirectory\SomeFile.txt"
if (Test-Path -Path $FilePath) {
# Check for string
}
}
}
For the most part, the pattern of Test-Connection and then Test-Path is effective and fast. There are certain computers, however, that ping successfully but Test-Path takes around 60 seconds to resolve to FALSE. I'm not sure why, but it may be a domain trust issue.
For situations like this, I would like to have a timeout for Test-Path that defaults to FALSE if it takes more than 2 seconds.
Unfortunately the solution in a related thread (How can I wrap this Powershell cmdlet into a timeout function?) does not apply to my situation. The proposed do-while loop gets hung up in the code block.
I've been experimenting with Jobs but it appears even this won't force quit the Test-Path command:
Start-Job -ScriptBlock {param($Path) Test-Path $Path} -ArgumentList $Path | Wait-Job -Timeout 2 | Remove-Job -Force
The job continues to hang in the background. Is this the cleanest way I can achieve my requirements above? Is there a better way to timeout Test-Path so the script doesn't hang besides spawning asynchronous activities? Many thanks.
Wrap your code in a [powershell] object and call BeginInvoke() to execute it asynchronously, then use the associated WaitHandle to wait for it to complete only for a set amount of time.
$sleepDuration = Get-Random 2,3
$ps = [powershell]::Create().AddScript("Start-Sleep -Seconds $sleepDuration; 'Done!'")
# execute it asynchronously
$handle = $ps.BeginInvoke()
# Wait 2500 milliseconds for it to finish
if(-not $handle.AsyncWaitHandle.WaitOne(2500)){
throw "timed out"
return
}
# WaitOne() returned $true, let's fetch the result
$result = $ps.EndInvoke($handle)
return $result
In the example above, we randomly sleep for either 2 or 3 seconds, but set a 2 and a half second timeout - try running it a couple of times to see the effect :)

How to resume bits transfer after network issue

I'm using asynchronous bits transfer to download a file from a remote server where I'm connected through the VPN.
Is there any way how to continue with downloading after VPN is disconnected?
When I start downloading then suspend my VPN and resume it after few seconds bits transfer fails with TransientError.
$transferCredentail = Get-Credential
$transferJob = Start-BitsTransfer -Source "\\remoteserver\path\filename" -Credential $transferCredentail -Destination C:\Temp -Description "Test" -Asynchronous
while (($transferJob.JobState -eq "Transferring") -or ($transferJob.JobState -eq "Connecting"))
{
Write-Output $transferJob.JobState
sleep 5;
}
Switch($transferJob.JobState)
{
"Transferred" {Complete-BitsTransfer -BitsJob $transferJob}
"Error" {$transferJob | Format-List } # List the errors.
default
{
Write-Output $transferJob.JobState
"Other action"
}
}
The solution was quite simple. Just check TransientError state in while loop. After some time the job state switched back to Transferring state and download completed successfully.
while (($transferJob.JobState -eq "Transferring") -or ($transferJob.JobState -eq "Connecting") -or ($transferJob.JobState -eq "TransientError"))
{
Write-Output $transferJob.JobState
sleep 5;
}
You can add the following in your switch statement for suspended jobs. I forget what state the bitstransfer job goes into when there are network issues (I think it's suspended) but if that doesn't work, you can add it to your default condition and add an if condition that handles anything other than the Transferring state.
Switch($transferJob.JobState)
{
"Transferred" {Complete-BitsTransfer -BitsJob $transferJob}
"Error" {$transferJob | Format-List } # List the errors.
"Suspended" {Get-BitsTransfer | Resume-BitsTransfer}
default
{
Write-Output $transferJob.JobState
"Other action"
}
}
This will resume all BitsTransfer jobs owned by the current user. If you had a name to the job, you can reference the name.

RDS User logoff Script Slow

With the help of the several online articles I was able to compile a powershell script that logs off all users for each of my RD Session hosts. I wanted something to be really gentle on logging off users and it writing profiles back to their roaming profile location on the storage system. However, this is too gentle and takes around four hours to complete with the amount of users and RDS servers I have.
This script is designed to set each RDS server drain but allow redirection if a server is available so the thought around this was within the first 15 minutes I would have the first few servers ready for users to log into.
All of this works but I would like like to see if there are any suggestions on speeding this up a little.
Here is the loop that goes through each server and logs users out and then sets the server logon mode to enabled:
ForEach ($rdsserver in $rdsservers){
try {
query user /server:$rdsserver 2>&1 | select -skip 1 | ? {($_ -split "\s+")[-5]} | % {logoff ($_ -split "\s+")[-6] /server:$rdsserver /V}
Write-Host "Giving the RDS Server time"
Write-Progress "Pausing Script" -status "Giving $rdsserver time to settle" -perc (5/(5/100))
Start-Sleep -Seconds 5
$RDSH=Get-WmiObject -Class "Win32_TerminalServiceSetting" -Namespace "root\CIMV2\terminalservices" -ComputerName $rdsserver -Authentication PacketPrivacy -Impersonation Impersonate
$RDSH.SessionBrokerDrainMode=0
$RDSH.put() > $null
Write-Host "$rdsserver is set to:"
switch ($RDSH.SessionBrokerDrainMode) {
0 {"Allow all connections."}
1 {"Allow incoming reconnections but until reboot prohibit new connections."}
2 {"Allow incoming reconnections but prohibit new connections."}
default {"The user logon state cannot be determined."}
}
}
catch {}
}
Not sure how many Servers you have but if its less than 50 or so you can do this in parallel with PSJobs. You'll have to wrap your code in a scriptblock, launch each server as a separate job, then wait for them to complete and retrieve any data returned. You won't be able to use Write-Host when doing this but I've swapped those to Out-Files. I also didn't parse out your code for collecting your list of servers but I'm going to assume that works and you can have it return a formatted list to a variable $rdsservers. You'll probably also want to modify the messages a bit so you can tell which server is which in the log file, or do different logs for each server. If you want anything other than the names of jobs to hit the console you'll have to output it with Write-Output or a return statement.
$SB = {
param($rdsserver)
Start-Sleep -Seconds 5
$RDSH=Get-WmiObject -Class "Win32_TerminalServiceSetting" -Namespace "root\CIMV2\terminalservices" -ComputerName $rdsserver -Authentication PacketPrivacy -Impersonation Impersonate
$RDSH.SessionBrokerDrainMode=0
$RDSH.put() > $null
"$rdsserver is set to:" | out-file $LogPath #Set this to whatever you want
switch ($RDSH.SessionBrokerDrainMode) {
0 {"Allow all connections." | out-file $LogPath}
1 {"Allow incoming reconnections but until reboot prohibit new connections." | out-file $LogPath}
2 {"Allow incoming reconnections but prohibit new connections." | out-file $LogPath}
default {"The user logon state cannot be determined." | out-file $LogPath}
}
foreach ($server in $rdsservers){
Start-Job -Scriptblock -ArgumentList $server
}
Get-Job | Wait-Job | Receive-Job
The foreach loop launches the jobs and then the last line waits for all of them to complete before getting any data that was output. You can also set a timeout on the wait if there is a chance your script never completes. If you've got a ton of boxes you may want to look into runspaces over jobs as they have better performance but take more work to use. This Link can help you out if you decide to go that way. I don't have an RDS deployment at the moment to test on so if you get any errors or have trouble getting it to work just post a comment and I'll see what I can do.
I have something ready for testing but it may break fantastically. You wizards out there may look at this and laugh. If i did this wrong please let me know.
$Serverperbatch = 2
$job = 0
$job = $Serverperbatch - 1
$batch = 1
While ($job -lt $rdsservers.count) {
$ServerBatch = $rdsservers[$job .. $job]
$jobname = "batch$batch"
Start-job -Name $jobname -ScriptBlock {
param ([string[]]$rdsservers)
Foreach ($rdsserver in $rdsservers) {
try {
query user /server:$rdsserver 2>&1 | select -skip 1 | ? {($_ -split "\s+")[-5]} | % {logoff ($_ -split "\s+")[-6] /server:$rdsserver /V}
$RDSH=Get-WmiObject -Class "Win32_TerminalServiceSetting" -Namespace "root\CIMV2\terminalservices" -ComputerName $rdsserver -Authentication PacketPrivacy -Impersonation Impersonate
$RDSH.SessionBrokerDrainMode=0
$RDSH.put() > $null
}
catch {}
} -ArgumentList (.$serverbatch)
$batch += 1
$Job = $job + 1
$job += $serverperbatch
If ($Job -gt $rdsservers.Count) {$Job = $rdsservers.Count}
If ($Job -gt $rdsservers.Count) {$Job = $rdsservers.Count}
}
}
Get-Job | Wait-Job | Receive-Job