Skip to content

Instantly share code, notes, and snippets.

@davidlu1001
Last active December 15, 2024 20:55
Show Gist options
  • Select an option

  • Save davidlu1001/b99c4b3a6e52bfbf69e36af09857a422 to your computer and use it in GitHub Desktop.

Select an option

Save davidlu1001/b99c4b3a6e52bfbf69e36af09857a422 to your computer and use it in GitHub Desktop.
Service State Monitor
# Enhanced Service State Monitor with Automatic gMSA Detection
# Version: 1.0 - PowerShell 5 Compatible
# Supports automated execution via Task Scheduler
#Requires -Version 5.0
#Requires -RunAsAdministrator
#Requires -Modules ActiveDirectory
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
[string]$serviceName = ".",
[Parameter(ValueFromPipeline = $true)]
[string]$serviceNameNotInclude = "",
[ValidateSet('Running', 'Stopped', 'StartPending', 'StopPending', 'ContinuePending', 'PausePending', 'Paused')]
[string]$checkState,
[ValidateSet('list', 'start', 'stop')]
[string]$ops = "list",
[ValidateScript({
if ([string]::IsNullOrEmpty($_)) { return $true }
$path = Split-Path -Path $_ -Parent
if (-not (Test-Path -Path $path)) {
New-Item -ItemType Directory -Path $path -Force | Out-Null
}
return $true
})]
[string]$logFile = "C:\temp\scripts\logs\checkServiceState.log",
[switch]$help,
[Parameter(Mandatory = $false)]
[ValidateRange(1, 10)]
[int]$gMSARetryAttempts = 2,
[Parameter(Mandatory = $false)]
[ValidateRange(5, 60)]
[int]$retryDelay = 10,
[Parameter(Mandatory = $false)]
[switch]$includeDependencies,
[Parameter(Mandatory = $false)]
[int]$serviceTimeout = 30
)
function ShowHelp {
@"
Windows Service Monitor with gMSA Support
Version 2.1 - PowerShell 5.1 Compatible
DESCRIPTION
Monitors and manages Windows Services with special handling for gMSA accounts.
Detects gMSA services automatically and handles credential refresh when needed.
Supports service dependencies and status monitoring.
SYNTAX
.\ServiceMonitor.ps1
[-serviceName <String>]
[-serviceNameNotInclude <String>]
[-checkState <String>]
[-ops <String>]
[-logFile <String>]
[-gMSARetryAttempts <Int>]
[-retryDelay <Int>]
[-includeDependencies]
[-serviceTimeout <Int>]
[-help]
PARAMETERS
-serviceName
Service name or pattern to match. Supports regular expressions.
Default: "." (matches all services)
-serviceNameNotInclude
Pattern to exclude services. Supports regular expressions.
Example: "Update|Search" excludes services containing these words
-checkState
Filter services by specific state.
Valid values: Running, Stopped, StartPending, StopPending,
ContinuePending, PausePending, Paused
-ops
Operation to perform.
Valid values: list, start, stop
Default: list
-logFile
Path for the log file.
Default: "C:\temp\scripts\logs\checkServiceState.log"
-gMSARetryAttempts
Number of retry attempts for gMSA reset.
Range: 1-10
Default: 2
-retryDelay
Delay between retry attempts in seconds.
Range: 5-60
Default: 10
-includeDependencies
Process service dependencies.
Default: False
-serviceTimeout
Timeout in seconds for service operations.
Default: 30
-help
Displays this help information.
EXAMPLES
# List all services
.\ServiceMonitor.ps1
# List specific service
.\ServiceMonitor.ps1 -serviceName "SQLServer"
# List all stopped SQL services
.\ServiceMonitor.ps1 -serviceName "SQL" -checkState Stopped -ops list
# Start a specific service
.\ServiceMonitor.ps1 -serviceName "SQLServer" -ops start
# Start service with dependencies
.\ServiceMonitor.ps1 -serviceName "SQLServer" -ops start -includeDependencies
# Stop services matching pattern
.\ServiceMonitor.ps1 -serviceName "SQL" -ops stop
# List services excluding pattern
.\ServiceMonitor.ps1 -serviceName "Windows" -serviceNameNotInclude "Update"
# Start service with custom retry settings
.\ServiceMonitor.ps1 -serviceName "SQLServer" -ops start -gMSARetryAttempts 5 -retryDelay 15
# Monitor services with custom timeout
.\ServiceMonitor.ps1 -serviceName "Critical" -serviceTimeout 60
NOTES
- Requires administrative privileges
- Requires ActiveDirectory PowerShell module
- Automatically detects and handles gMSA accounts
- Creates detailed logs of all operations
- Supports regular expressions for service name matching
- Thread-safe operations for concurrent execution
- Handles service dependencies if specified
- Provides detailed operation logging
- Supports automatic retry for failed operations
- Maintains operation status counts
"@ | Write-Output
}
if ($help) {
ShowHelp
return
}
# Initialize script-level variables
$script:serviceCache = @{}
$script:serviceCacheLock = New-Object System.Object
$script:ErrorActionPreference = 'Stop'
$script:VerbosePreference = 'Continue'
# Enhanced logging function with error handling
function Write-Log {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Message,
[ValidateSet('Information', 'Warning', 'Error', 'Debug', 'Success')]
[string]$Level = 'Information',
[switch]$NoConsole
)
try {
[System.Threading.Monitor]::Enter($script:serviceCacheLock)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
$logMessage = "$timestamp [$Level] $Message"
if (-not [string]::IsNullOrEmpty($logFile)) {
$logDir = Split-Path -Path $logFile -Parent
if (-not (Test-Path -Path $logDir)) {
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}
Add-Content -Path $logFile -Value $logMessage -ErrorAction Stop
}
if (-not $NoConsole) {
switch ($Level) {
'Error' { Write-Host $logMessage -ForegroundColor Red }
'Warning' { Write-Host $logMessage -ForegroundColor Yellow }
'Success' { Write-Host $logMessage -ForegroundColor Green }
'Debug' { Write-Verbose $logMessage }
default { Write-Host $logMessage }
}
}
}
catch {
Write-Error "Failed to write log: $_"
}
finally {
[System.Threading.Monitor]::Exit($script:serviceCacheLock)
}
}
# Thread-safe cache operations
function Get-CacheValue {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Key
)
try {
[System.Threading.Monitor]::Enter($script:serviceCacheLock)
return $script:serviceCache[$Key]
}
finally {
[System.Threading.Monitor]::Exit($script:serviceCacheLock)
}
}
function Set-CacheValue {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Key,
[Parameter(Mandatory = $true)]
$Value
)
try {
[System.Threading.Monitor]::Enter($script:serviceCacheLock)
$script:serviceCache[$Key] = $Value
}
finally {
[System.Threading.Monitor]::Exit($script:serviceCacheLock)
}
}
# Function to wait for service status change
function Wait-ServiceStatus {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $true)]
[string]$DesiredStatus,
[int]$TimeoutSeconds = 30
)
try {
$service = Get-Service -Name $ServiceName
$timer = [System.Diagnostics.Stopwatch]::StartNew()
while ($service.Status -ne $DesiredStatus -and $timer.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
Start-Sleep -Milliseconds 500
$service.Refresh()
Write-Log -Message "Waiting for service $ServiceName to reach $DesiredStatus state (Current: $($service.Status))" -Level Debug -NoConsole
}
$timer.Stop()
if ($service.Status -ne $DesiredStatus) {
Write-Log -Message "Service $ServiceName did not reach $DesiredStatus state within $TimeoutSeconds seconds" -Level Warning
return $false
}
return $true
}
catch {
Write-Log -Message "Error waiting for service status: $_" -Level Error
return $false
}
}
# Function to detect if an account is a gMSA
function Test-IsGMSAAccount {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$AccountName
)
try {
if ([string]::IsNullOrEmpty($AccountName)) {
return $false
}
$cacheKey = "GMSA_$AccountName"
$cachedValue = Get-CacheValue -Key $cacheKey
if ($null -ne $cachedValue) {
return $cachedValue
}
$cleanAccountName = $AccountName -replace '^.*\\', '' -replace '\$$', ''
$account = Get-ADServiceAccount -Identity $cleanAccountName -ErrorAction SilentlyContinue
$isGMSA = ($null -ne $account)
Set-CacheValue -Key $cacheKey -Value $isGMSA
return $isGMSA
}
catch {
Write-Log -Message "Error checking if $AccountName is a gMSA: $_" -Level Debug
return $false
}
}
# Function to get service account
function Get-ServiceAccount {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ServiceName
)
try {
$cacheKey = "SVC_$ServiceName"
$cachedValue = Get-CacheValue -Key $cacheKey
if ($null -ne $cachedValue) {
return $cachedValue
}
$service = Get-WmiObject -Class Win32_Service -Filter "Name='$ServiceName'" -ErrorAction Stop
if ($null -ne $service) {
Set-CacheValue -Key $cacheKey -Value $service.StartName
return $service.StartName
}
}
catch {
Write-Log -Message "Error getting service account for $ServiceName : $_" -Level Error
}
return $null
}
# Function to get service dependencies
function Get-ServiceDependencies {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ServiceName
)
try {
$service = Get-Service -Name $ServiceName -ErrorAction Stop
$dependencies = New-Object System.Collections.ArrayList
function Get-DependenciesRecursive {
param ($ServiceObj)
foreach ($depService in $ServiceObj.ServicesDependedOn) {
if (-not $dependencies.Contains($depService.Name)) {
[void]$dependencies.Add($depService.Name)
Get-DependenciesRecursive $depService
}
}
}
Get-DependenciesRecursive $service
return $dependencies.ToArray()
}
catch {
Write-Log -Message "Error getting dependencies for $ServiceName : $_" -Level Error
return @()
}
}
# Function to reset gMSA credentials
function Reset-ServiceGMSACredentials {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $true)]
[string]$ServiceAccount,
[Parameter(Mandatory = $false)]
[int]$MaxRetryAttempts = 3,
[Parameter(Mandatory = $false)]
[int]$RetryDelaySeconds = 5,
[Parameter(Mandatory = $false)]
[int]$TimeoutSeconds = 30
)
try {
Write-Log -Message "Starting gMSA credential reset for service: $ServiceName" -Level Information
# Validate service exists
$service = Get-Service -Name $ServiceName -ErrorAction Stop
Write-Log -Message "Current service status: $($service.Status)" -Level Debug
# Check if service is using the specified gMSA account
$currentAccount = Get-ServiceAccount -ServiceName $ServiceName
if ($currentAccount -ne $ServiceAccount) {
Write-Log -Message "Service account mismatch. Current: $currentAccount, Expected: $ServiceAccount" -Level Warning
return $false
}
# Stop service and its dependencies
if ($service.Status -ne 'Stopped') {
# Handle dependencies first
if ($includeDependencies) {
$dependencies = Get-ServiceDependencies -ServiceName $ServiceName
foreach ($dep in $dependencies) {
Write-Log -Message "Stopping dependency: $dep" -Level Debug
Stop-Service -Name $dep -Force -ErrorAction SilentlyContinue
Wait-ServiceStatus -ServiceName $dep -DesiredStatus 'Stopped' -TimeoutSeconds $TimeoutSeconds
}
}
# Stop main service
Write-Log -Message "Stopping service $ServiceName"
Stop-Service -Name $ServiceName -Force -ErrorAction Stop
if (-not (Wait-ServiceStatus -ServiceName $ServiceName -DesiredStatus 'Stopped' -TimeoutSeconds $TimeoutSeconds)) {
throw "Failed to stop service $ServiceName within timeout period"
}
}
# Reset service password using sc.exe
Write-Log -Message "Resetting service password"
$scCommand = "sc.exe config `"$ServiceName`" obj= `"$ServiceAccount`" password= `"`""
$scResult = Invoke-Expression -Command $scCommand 2>&1
if ($LASTEXITCODE -ne 0) {
throw "Failed to reset service password. Error: $scResult"
}
# Attempt to start the service with retries
$attempt = 1
$serviceStarted = $false
while (-not $serviceStarted -and $attempt -le $MaxRetryAttempts) {
Write-Log -Message "Start attempt $attempt of $MaxRetryAttempts"
try {
# Start dependencies first if needed
if ($includeDependencies) {
foreach ($dep in $dependencies) {
Start-Service -Name $dep -ErrorAction SilentlyContinue
Wait-ServiceStatus -ServiceName $dep -DesiredStatus 'Running' -TimeoutSeconds $TimeoutSeconds
}
}
# Start main service
Start-Service -Name $ServiceName -ErrorAction Stop
if (Wait-ServiceStatus -ServiceName $ServiceName -DesiredStatus 'Running' -TimeoutSeconds $TimeoutSeconds) {
$serviceStarted = $true
Write-Log -Message "Service started successfully" -Level Success
break
}
}
catch {
Write-Log -Message "Attempt $attempt failed: $_" -Level Warning
if ($attempt -lt $MaxRetryAttempts) {
Start-Sleep -Seconds $RetryDelaySeconds
}
}
$attempt++
}
if (-not $serviceStarted) {
throw "Failed to start service after $MaxRetryAttempts attempts"
}
return $true
}
catch {
Write-Log -Message "Error during gMSA credential reset: $_" -Level Error
return $false
}
}
# Function to manage service operations
function Manage-Service {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ServiceName,
[Parameter(Mandatory = $true)]
[ValidateSet('start', 'stop', 'list')]
[string]$Operation,
[Parameter(Mandatory = $true)]
[string]$DesiredState
)
try {
$service = Get-Service -Name $ServiceName
$serviceAccount = Get-ServiceAccount -ServiceName $ServiceName
$isGMSA = Test-IsGMSAAccount -AccountName $serviceAccount
Write-Log -Message "Processing service: $ServiceName, Account: $serviceAccount, Is gMSA: $isGMSA" -Level Debug
switch ($Operation) {
'start' {
if ($service.Status -eq 'Stopped') {
if ($includeDependencies) {
$dependencies = Get-ServiceDependencies -ServiceName $ServiceName
foreach ($dep in $dependencies) {
Manage-Service -ServiceName $dep -Operation 'start' -DesiredState 'Running'
}
}
Write-Log -Message "Attempting to start service: $ServiceName"
Start-Service -Name $ServiceName -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
$service.Refresh()
if ($service.Status -ne 'Running' -and $isGMSA) {
Write-Log -Message "Normal start failed, attempting gMSA reset"
for ($i = 1; $i -le $gMSARetryAttempts; $i++) {
Write-Log -Message "gMSA reset attempt $i of $gMSARetryAttempts" -Level Debug
if (Reset-ServiceGMSACredentials -ServiceName $ServiceName -ServiceAccount $serviceAccount) {
return $true
}
if ($i -lt $gMSARetryAttempts) {
Start-Sleep -Seconds $retryDelay
}
}
return $false
}
$startResult = Wait-ServiceStatus -ServiceName $ServiceName -DesiredStatus 'Running' -TimeoutSeconds $serviceTimeout
return $startResult
}
return $true
}
'stop' {
if ($service.Status -eq 'Running') {
if ($includeDependencies) {
$dependentServices = Get-Service |
Where-Object { $_.ServicesDependedOn |
Where-Object { $_.Name -eq $ServiceName }
}
foreach ($depService in $dependentServices) {
Stop-Service -Name $depService.Name -Force -ErrorAction SilentlyContinue
Wait-ServiceStatus -ServiceName $depService.Name -DesiredStatus 'Stopped' -TimeoutSeconds $serviceTimeout
}
}
Stop-Service -Name $ServiceName -Force -ErrorAction Stop
$stopResult = Wait-ServiceStatus -ServiceName $ServiceName -DesiredStatus 'Stopped' -TimeoutSeconds $serviceTimeout
return $stopResult
}
return $true
}
'list' { return $true }
}
}
catch {
Write-Log -Message "Error managing service $ServiceName : $_" -Level Error
return $false
}
}
# Show help if requested
if ($help) {
. $PSScriptRoot\ShowHelp.ps1
return
}
# Main execution block continued
try {
Write-Log -Message "Script started with operation: $ops"
# Input validation
if ($serviceName -match '[^\w\d\.\-\*\?\[\]\(\)\|\\]') {
throw "Invalid service name pattern detected. Please use valid characters only."
}
# Get matching services with error handling
$services = @()
if (-not [string]::IsNullOrEmpty($serviceNameNotInclude)) {
$services = @(Get-Service | Where-Object {
$_.DisplayName -match $serviceName -and
$_.DisplayName -notmatch $serviceNameNotInclude
})
}
else {
$services = @(Get-Service | Where-Object {
$_.DisplayName -match $serviceName
})
}
if ($services.Count -eq 0) {
Write-Log -Message "No services found matching criteria" -Level Warning
return
}
Write-Log -Message "Found $($services.Count) matching services" -Level Debug
# Process services based on operation
switch ($ops) {
"list" {
$targetServices = @()
if ($checkState) {
$targetServices = @($services | Where-Object { $_.Status -eq $checkState })
}
else {
$targetServices = $services
}
foreach ($svc in $targetServices) {
$account = Get-ServiceAccount -ServiceName $svc.Name
$isGMSA = Test-IsGMSAAccount -AccountName $account
$dependencies = @()
if ($includeDependencies) {
$dependencies = Get-ServiceDependencies -ServiceName $svc.Name
}
$dependenciesText = if ($dependencies.Count -gt 0) {
$dependencies -join ', '
}
else {
'None'
}
$serviceDetails = @"
Service Details:
Name: $($svc.DisplayName)
Service Name: $($svc.Name)
Status: $($svc.Status)
Account: $account
Is gMSA: $isGMSA
Dependencies: $dependenciesText
"@
Write-Log -Message $serviceDetails
}
Write-Log -Message "Listed $($targetServices.Count) services" -Level Success
}
"start" {
$servicesToStart = @($services | Where-Object { $_.Status -eq 'Stopped' })
if ($servicesToStart.Count -gt 0) {
$successCount = 0
$failCount = 0
foreach ($svc in $servicesToStart) {
Write-Log -Message "Processing start operation for $($svc.DisplayName)"
$result = Manage-Service -ServiceName $svc.Name -Operation 'start' -DesiredState 'Running'
if ($result) {
$successCount++
Write-Log -Message "Successfully started service: $($svc.DisplayName)" -Level Success
}
else {
$failCount++
Write-Log -Message "Failed to start service: $($svc.DisplayName)" -Level Error
}
}
Write-Log -Message "Start operations completed. Success: $successCount, Failed: $failCount" -Level Information
}
else {
Write-Log -Message "No stopped services found to start" -Level Information
}
}
"stop" {
$servicesToStop = @($services | Where-Object { $_.Status -eq 'Running' })
if ($servicesToStop.Count -gt 0) {
$successCount = 0
$failCount = 0
foreach ($svc in $servicesToStop) {
Write-Log -Message "Processing stop operation for $($svc.DisplayName)"
$result = Manage-Service -ServiceName $svc.Name -Operation 'stop' -DesiredState 'Stopped'
if ($result) {
$successCount++
Write-Log -Message "Successfully stopped service: $($svc.DisplayName)" -Level Success
}
else {
$failCount++
Write-Log -Message "Failed to stop service: $($svc.DisplayName)" -Level Error
}
}
Write-Log -Message "Stop operations completed. Success: $successCount, Failed: $failCount" -Level Information
}
else {
Write-Log -Message "No running services found to stop" -Level Information
}
}
}
}
catch {
Write-Log -Message "Critical error in script execution: $_" -Level Error
throw $_
}
finally {
# Cleanup
try {
if ($script:serviceCache) {
$script:serviceCache.Clear()
}
Write-Log -Message "Script execution completed" -Level Information
}
catch {
Write-Error "Error during cleanup: $_"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment