33# Licensed under a 3-clause BSD style license (see LICENSE)
44import ctypes
55import re
6+ import time
67
8+ import psutil
79import pywintypes
810import win32service
911import winerror
@@ -142,6 +144,8 @@ def __str__(self):
142144 @property
143145 def hSvc (self ):
144146 if self ._hSvc is None :
147+ # Handle will automatically be closed by pywin32
148+ # https://mhammond.github.io/pywin32/PySC_HANDLE.html
145149 self ._hSvc = win32service .OpenService (self .scm_handle , self .name , win32service .SERVICE_QUERY_CONFIG )
146150 return self ._hSvc
147151
@@ -204,6 +208,21 @@ def startup_type_string(self):
204208 return startup_type_string
205209
206210
211+ def _build_process_cache () -> dict [int , "psutil.Process" ]:
212+ process_cache_dict = {}
213+ for proc in psutil .process_iter (attrs = ['pid' , 'create_time' ]):
214+ process_cache_dict [proc .pid ] = proc
215+ return process_cache_dict
216+
217+
218+ def _get_process_uptime_from_cache (pid : int , process_cache : dict [int , "psutil.Process" ]) -> int :
219+ process = process_cache .get (pid )
220+ if process is None :
221+ return 0
222+
223+ return int (time .time () - process .create_time ())
224+
225+
207226class WindowsService (AgentCheck ):
208227 SERVICE_CHECK_NAME = 'windows_service.state'
209228 # https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_status_process
@@ -225,6 +244,7 @@ class WindowsService(AgentCheck):
225244 win32service .SERVICE_PAUSE_PENDING : "pause_pending" ,
226245 win32service .SERVICE_PAUSED : "paused" ,
227246 }
247+ UNKNOWN_LITERAL = "unknown"
228248
229249 def check (self , instance ):
230250 services = instance .get ('services' , [])
@@ -243,14 +263,21 @@ def check(self, instance):
243263 services_unseen = {f .name for f in service_filters if f .name is not None }
244264
245265 try :
266+ # Handle will automatically be closed by pywin32
267+ # https://mhammond.github.io/pywin32/PySC_HANDLE.html
246268 scm_handle = win32service .OpenSCManager (None , None , win32service .SC_MANAGER_ENUMERATE_SERVICE )
247269 except Exception as e : # no cov
248270 raise Exception ('Unable to open SCManager: {}' .format (e ))
249271
250- type_filter = win32service .SERVICE_WIN32
251- state_filter = win32service .SERVICE_STATE_ALL
272+ service_status_process_enums = win32service .EnumServicesStatusEx (
273+ scm_handle ,
274+ win32service .SERVICE_WIN32 ,
275+ win32service .SERVICE_STATE_ALL ,
276+ None ,
277+ win32service .SC_ENUM_PROCESS_INFO ,
278+ )
252279
253- service_statuses = win32service . EnumServicesStatus ( scm_handle , type_filter , state_filter )
280+ process_cache = _build_process_cache ( )
254281
255282 # Sort service filters in reverse order on the regex pattern so more specific (longer)
256283 # regex patterns are tested first. This is to handle cases when a pattern is a prefix of
@@ -261,8 +288,13 @@ def check(self, instance):
261288 # See test_name_regex_order()
262289 service_filters = sorted (service_filters , reverse = True , key = lambda x : len (x .name or "" ))
263290
264- for short_name , display_name , service_status in service_statuses :
265- service_view = ServiceView (scm_handle , short_name )
291+ for service_status_process_enum in service_status_process_enums :
292+ service_name = service_status_process_enum ["ServiceName" ]
293+ display_name = service_status_process_enum ["DisplayName" ]
294+ state = service_status_process_enum ["CurrentState" ]
295+ service_pid = service_status_process_enum ["ProcessId" ]
296+
297+ service_view = ServiceView (scm_handle , service_name )
266298
267299 if 'ALL' not in services :
268300 for service_filter in service_filters :
@@ -276,16 +308,20 @@ def check(self, instance):
276308 except (pywintypes .error , OSError ) as e :
277309 self .log .exception ("Exception at service match for %s" , service_filter )
278310 self .warning (
279- "Failed to query %s service config for filter %s: %s" , short_name , service_filter , str (e )
311+ "Failed to query %s service config for filter %s: %s" , service_name , service_filter , str (e )
280312 )
281313 else :
282314 continue
283315
284- state = service_status [1 ]
316+ service_uptime = 0
317+ # If service_pid is 0, the service is not running
318+ if service_pid != 0 :
319+ service_uptime = _get_process_uptime_from_cache (service_pid , process_cache )
320+
285321 status = self .STATE_TO_STATUS .get (state , self .UNKNOWN )
286- state_string = self .STATE_TO_STRING .get (state , "unknown" )
322+ state_string = self .STATE_TO_STRING .get (state , self . UNKNOWN_LITERAL )
287323
288- tags = ['windows_service:{}' .format (short_name ), 'windows_service_state:{}' .format (state_string )]
324+ tags = ['windows_service:{}' .format (service_name ), 'windows_service_state:{}' .format (state_string )]
289325 tags .extend (custom_tags )
290326
291327 if instance .get ('collect_display_name_as_tag' , False ):
@@ -297,22 +333,23 @@ def check(self, instance):
297333 except pywintypes .error as e :
298334 self .log .exception ("Exception at windows_service_startup_type tag for %s" , service_filter )
299335 self .warning (
300- "Failed to query %s service config for filter %s: %s" , short_name , service_filter , str (e )
336+ "Failed to query %s service config for filter %s: %s" , service_name , service_filter , str (e )
301337 )
302338
303339 if not instance .get ('disable_legacy_service_tag' , False ):
304340 self ._log_deprecation ('service_tag' , 'windows_service' )
305- tags .append ('service:{}' .format (short_name ))
341+ tags .append ('service:{}' .format (service_name ))
306342
307343 self .service_check (self .SERVICE_CHECK_NAME , status , tags = tags )
308- self .log .debug ('service state for %s %s' , short_name , status )
344+ self .log .debug ('service state for %s %s' , service_name , status )
345+ self .gauge ('windows_service.uptime' , service_uptime , tags = tags )
309346
310347 if 'ALL' not in services :
311348 for service in services_unseen :
312349 # if a name doesn't match anything (wrong name or no permission to access the service), report UNKNOWN
313350 status = self .UNKNOWN
314351
315- tags = ['windows_service:{}' .format (service ), 'windows_service_state:{}' .format ("unknown" )]
352+ tags = ['windows_service:{}' .format (service ), 'windows_service_state:{}' .format (self . UNKNOWN_LITERAL )]
316353
317354 tags .extend (custom_tags )
318355
0 commit comments