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

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
}
}

Related

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.

Check Windows Services status using PowerShell

By writing a script in PowerShell, I'd like to determine whether a Windows service is running or not running. For this, I have constructed the following script:
#Variables
$winupdate = 'Windows Update'
$running = 'Running'
#Function
function CheckServiceStatus {
param($winupdate)
$getservice = Get-Service -Name $winupdate
if($getservice.Status -ne $running){
Start-Service $winupdate
Write-host "Starting" $winupdate "service"|out-file "C:\Users\ArifSohM\Desktop\Stuff for PowerShell\results.txt"
}
}
To interpret, what I've tried to do here is simply, create a function called "CheckServiceStatus". Within that function, I have created a parameter and placed the variable of the Windows service name within this parameter. Then, I have placed the "Get-Service" cmdlet into another variable called "$getservice". I then went onto starting an IF statement that is supposed to check if the service is running, so what I've said here is, if the service is NOT running, start the service, create a text file and output a confirmation message into it.
After hitting run on the above script, nothing seems to happen. Am I doing something wrong? Am I missing something? Any help will be much appreciated!
you are using service display name instead of service name.
the service name of windows update "wuauserv"
change the winupdate variable part and you should be fine
$winupdate = 'wuauserv'
function definition should be before calling that function
and function call statement
full code:
function CheckServiceStatus {
param($winupdate)
$getservice = Get-Service -Name $winupdate
if($getservice.Status -ne $running){
Start-Service $winupdate
Write-output "Starting" $winupdate "service"|out-file "C:\Users\ArifSohM\Desktop\Stuff for PowerShell\results.txt"
}
}
#Variables
$winupdate = 'wuauserv'
$running = 'Running'
CheckServiceStatus $winupdate

Better way of writing a script to avoid boolean error

I have written a script and as part of the script I am checking if a job is running and if it is, forcing it to stop:
$copyjob = Get-Job -Name DBACopy_QFR1-DBA20_2_QFR3-DBS21_S_Drv_DBA -ErrorAction SilentlyContinue
if ($copyjob)
{
remove-job $copyjob -force # This job my be causing problems with the backups so we kill it to make sure.
}
However, I think what I have written is the cause of this error when the script runs:
Cannot convert value "System.Management.Automation.PSRemotingJob" to type "System.Boolean". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0
Is there a better way of doing it that doesn't throw up that error, or am I completely incorrect in thinking that this is what is spawning the error.
Instead of
if ($copyjob)
{
}
try using
if ($copyjob -ne $null)
{
}
If that is all your code does, you can simplify that:
Remove-Job -Name DBACopy_QFR1-DBA20_2_QFR3-DBS21_S_Drv_DBA -ErrorAction SilentlyContinue
Less code, less chances to fail. You can also pipeline:
Get-Job -Name "SomeName" -ErrorAction SilentlyContinue | remove-job -force

Powershell job unexpectedly returning 'System.Management.Automation.PSObject' object instead of System.Object

I run a maintenance Powershell script which remotely checks Windows server event logs for various entries and then takes appropriate corrective/alerting actions.
The script runs every 5 minutes, but will occasionally run too long due to Get-WinEvent calls timing out with an RPC unavailable error while attempting to query unreachable/unresponsive servers.
To avoid this issue, I am working on wrapping the Get-WinEvent calls in Jobs so that I can apply a configurable timeout to them.
For Get-WinEvent jobs finding multiple events, Receive-Job properly returns a 'System.Object[]' array containing 'System.Diagnostics.Eventing.Reader.EventLogRecord' objects. If only a single event is found, Receive-Job returns a 'System.Management.Automation.PSObject' object instead.
Without the Job-related code, a Get-WinEvent call finding one event returns a non-array 'System.Diagnostics.Eventing.Reader.EventLogRecord' object which can easily be wrapped with an array for downstream consumption.
Anyone have a better way to add a timeout to a remote Get-WinEvent call or an explanation/fix for the 'System.Management.Automation.PSObject' being returned instead of a non-array 'System.Diagnostics.Eventing.Reader.EventLogRecord' object?
The function and some sample calls are shown below:
Function CollectRemoteEvents($the_server,$event_log,$events_to_find,$event_label,$search_start,$search_timeout,$max_event_count){
Try{
$job_info = Start-Job -name GetEvents -scriptblock {param($server,$logname,$eventID,$StartTime,$MaxEvents) Get-WinEvent -ComputerName $server -FilterHashtable #{"logname"=$logname;"id"=$eventID;StartTime=$StartTime} -MaxEvents $MaxEvents} -Arg $the_server,$event_log,$events_to_find,$search_start,$max_event_count
#if the provided timeout value is greater than 0, use it
if($search_timeout -gt 0){
#if the job takes a while, tell it to timeout after ## seconds
$wait_result = Wait-Job -id $job_info.id -timeout $search_timeout
}Else{
#if the timeout was specified as 0, let the job run to completion
$wait_result = Wait-Job -id $job_info.id
}
$current_job_state = Get-Job -id ($job_info.id)
#check if the job has completed before time runs out
if($current_job_state.State -eq "Completed"){
#capture the job object
$job = Get-Job -id ($job_info.id)
#retrieve the output of the job; if the job raises errors, exceptions will be populated into the $joberror variable
#NOTE: the $ is *intentionally* left out of the 'joberror' variable name in the command below
$job_result = $job | Receive-Job -ErrorVariable joberror -ErrorAction Stop
If($joberror -ne "" -And $joberror -ne $null){
#if joberror is not empty, the job failed; log it
# write-host "JobError: '$joberror'" #used for debugging, this would log to file in a production capacity
}Else{
# write-host $job_result.gettype() #used for debugging
return ,$job_result
}
}else{
#the search timed out
# write-host "The event log search timed out." #used for debugging, this would log to file in a production capacity
return $null
}
}Catch [Exception]{
If($_.FullyQualifiedErrorID -eq "NoMatchingEventsFound,Microsoft.PowerShell.Commands.GetWinEventCommand"){
#No logon timeout events were registered since $search_start
write-host "$the_server : No $event_label events were found."
return #()
}Elseif($_.FullyQualifiedErrorID -eq "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.GetWinEventCommand"){
#"argument validation error", exit the function with a return value indicating failure
write-host "$the_server : Event log retrieval failed, can't check for $event_label events (Argument validation error);"
return $null
}Elseif($_.FullyQualifiedErrorID -eq "System.Diagnostics.Eventing.Reader.EventLogException,Microsoft.PowerShell.Commands.GetWinEventCommand"){
#"the RPC server is unavailable", exit the function with a return value indicating failure
write-host "$the_server : Event log retrieval failed, can't check for $event_label events (RPC server unavailable);"
return $null
}Else{
#if the server logs cannot be retrieved, exit the function with a return value indicating failure
write-host "$the_server : Event log retrieval failed, can't check for $event_label events (Check access/permissions)($($_.FullyQualifiedErrorID));"
return $null
}
}
}
$server_name = "localhost"
$system_event_ID = #(6013)
$app_event_ID = #(1033)
$timeout_check_timespan = (Get-Date).AddMonths(-2)
$WinEvent_timeout = 10 #how long to let the Job run before timing out
$returns_array = CollectRemoteEvents $server_name 'System' $system_event_ID "Label One" $timeout_check_timespan $WinEvent_timeout 5
$returns_non_array = CollectRemoteEvents $server_name 'Application' $app_event_ID "Label Two" $timeout_check_timespan $WinEvent_timeout 1
write-host ""
write-host $returns_array
write-host $returns_array.count
write-host ""
write-host $returns_non_array
write-host $returns_non_array.count
The comma on the main return line is attempt to force an array to be returned (see: Count property of array in PowerShell with pscustomobjects )
I have also tried instantiating an array and then adding the result set to it:
$var = #()
$var += $results
return $var
casting the result set as an array:
return [Array]($results)
and returning the result set as part of an array:
return #($results)
I believe that this is a different issue than the one covered in the 'Function return value in Powershell' proposed solution - in my issue the problem of the object types is present before the function returns.
Uncommenting the following line for debugging purposes
# write-host $job_result.gettype() #used for debugging
Results in the following output being printed:
System.Object[]
System.Management.Automation.PSObject
The System.Object[] line is returned by a Job running a Get-WinEvent query that finds multiple events.
The 'System.Management.Automation.PSObject' line is returned by a Job running a Get-WinEvent query that finds a single event
After lots of googling based upon a suggestion from a Reddit user, it appears that you effectively have to double-wrap the single-object return content to have it end up as an array:
#this *does not* work
return #(#($job_result))
#This works
return , #($job_result)

Powershell Wait for service to be stopped or started

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
}