1r"""
2Salt Util for getting system information with the Performance Data Helper (pdh).
3Counter information is gathered from current activity or log files.
4
5Usage:
6
7.. code-block:: python
8
9    import salt.utils.win_pdh
10
11    # Get a list of Counter objects
12    salt.utils.win_pdh.list_objects()
13
14    # Get a list of ``Processor`` instances
15    salt.utils.win_pdh.list_instances('Processor')
16
17    # Get a list of ``Processor`` counters
18    salt.utils.win_pdh.list_counters('Processor')
19
20    # Get the value of a single counter
21    # \Processor(*)\% Processor Time
22    salt.utils.win_pdh.get_counter('Processor', '*', '% Processor Time')
23
24    # Get the values of multiple counters
25    counter_list = [('Processor', '*', '% Processor Time'),
26                    ('System', None, 'Context Switches/sec'),
27                    ('Memory', None, 'Pages/sec'),
28                    ('Server Work Queues', '*', 'Queue Length')]
29    salt.utils.win_pdh.get_counters(counter_list)
30
31    # Get all counters for the Processor object
32    salt.utils.win_pdh.get_all_counters('Processor')
33"""
34
35# https://docs.microsoft.com/en-us/windows/desktop/perfctrs/using-the-pdh-functions-to-consume-counter-data
36
37# https://www.cac.cornell.edu/wiki/index.php?title=Performance_Data_Helper_in_Python_with_win32pdh
38import logging
39import time
40
41import salt.utils.platform
42from salt.exceptions import CommandExecutionError
43
44try:
45    import pywintypes
46    import win32pdh
47
48    HAS_WINDOWS_MODULES = True
49except ImportError:
50    HAS_WINDOWS_MODULES = False
51
52
53log = logging.getLogger(__file__)
54
55# Define the virtual name
56__virtualname__ = "pdh"
57
58
59def __virtual__():
60    """
61    Only works on Windows systems with the PyWin32
62    """
63    if not salt.utils.platform.is_windows():
64        return False, "salt.utils.win_pdh: Requires Windows"
65
66    if not HAS_WINDOWS_MODULES:
67        return False, "salt.utils.win_pdh: Missing required modules"
68
69    return __virtualname__
70
71
72class Counter:
73    """
74    Counter object
75    Has enumerations and functions for working with counters
76    """
77
78    # The dwType field from GetCounterInfo returns the following, or'ed.
79    # These come from WinPerf.h
80    PERF_SIZE_DWORD = 0x00000000
81    PERF_SIZE_LARGE = 0x00000100
82    PERF_SIZE_ZERO = 0x00000200  # for Zero Length fields
83    PERF_SIZE_VARIABLE_LEN = 0x00000300
84    # length is in the CounterLength field of the Counter Definition structure
85
86    # select one of the following values to indicate the counter field usage
87    PERF_TYPE_NUMBER = 0x00000000  # a number (not a counter)
88    PERF_TYPE_COUNTER = 0x00000400  # an increasing numeric value
89    PERF_TYPE_TEXT = 0x00000800  # a text field
90    PERF_TYPE_ZERO = 0x00000C00  # displays a zero
91
92    # If the PERF_TYPE_NUMBER field was selected, then select one of the
93    # following to describe the Number
94    PERF_NUMBER_HEX = 0x00000000  # display as HEX value
95    PERF_NUMBER_DECIMAL = 0x00010000  # display as a decimal integer
96    PERF_NUMBER_DEC_1000 = 0x00020000  # display as a decimal/1000
97
98    # If the PERF_TYPE_COUNTER value was selected then select one of the
99    # following to indicate the type of counter
100    PERF_COUNTER_VALUE = 0x00000000  # display counter value
101    PERF_COUNTER_RATE = 0x00010000  # divide ctr / delta time
102    PERF_COUNTER_FRACTION = 0x00020000  # divide ctr / base
103    PERF_COUNTER_BASE = 0x00030000  # base value used in fractions
104    PERF_COUNTER_ELAPSED = 0x00040000  # subtract counter from current time
105    PERF_COUNTER_QUEUE_LEN = 0x00050000  # Use Queue len processing func.
106    PERF_COUNTER_HISTOGRAM = 0x00060000  # Counter begins or ends a histogram
107
108    # If the PERF_TYPE_TEXT value was selected, then select one of the
109    # following to indicate the type of TEXT data.
110    PERF_TEXT_UNICODE = 0x00000000  # type of text in text field
111    PERF_TEXT_ASCII = 0x00010000  # ASCII using the CodePage field
112
113    #  Timer SubTypes
114    PERF_TIMER_TICK = 0x00000000  # use system perf. freq for base
115    PERF_TIMER_100NS = 0x00100000  # use 100 NS timer time base units
116    PERF_OBJECT_TIMER = 0x00200000  # use the object timer freq
117
118    # Any types that have calculations performed can use one or more of the
119    # following calculation modification flags listed here
120    PERF_DELTA_COUNTER = 0x00400000  # compute difference first
121    PERF_DELTA_BASE = 0x00800000  # compute base diff as well
122    PERF_INVERSE_COUNTER = 0x01000000  # show as 1.00-value (assumes:
123    PERF_MULTI_COUNTER = 0x02000000  # sum of multiple instances
124
125    # Select one of the following values to indicate the display suffix (if any)
126    PERF_DISPLAY_NO_SUFFIX = 0x00000000  # no suffix
127    PERF_DISPLAY_PER_SEC = 0x10000000  # "/sec"
128    PERF_DISPLAY_PERCENT = 0x20000000  # "%"
129    PERF_DISPLAY_SECONDS = 0x30000000  # "secs"
130    PERF_DISPLAY_NO_SHOW = 0x40000000  # value is not displayed
131
132    def build_counter(obj, instance, instance_index, counter):
133        r"""
134        Makes a fully resolved counter path. Counter names are formatted like
135        this:
136
137        ``\Processor(*)\% Processor Time``
138
139        The above breaks down like this:
140
141            obj = 'Processor'
142            instance = '*'
143            counter = '% Processor Time'
144
145        Args:
146
147            obj (str):
148                The top level object
149
150            instance (str):
151                The instance of the object
152
153            instance_index (int):
154                The index of the instance. Can usually be 0
155
156            counter (str):
157                The name of the counter
158
159        Returns:
160            Counter: A Counter object with the path if valid
161
162        Raises:
163            CommandExecutionError: If the path is invalid
164        """
165        path = win32pdh.MakeCounterPath(
166            (None, obj, instance, None, instance_index, counter), 0
167        )
168        if win32pdh.ValidatePath(path) == 0:
169            return Counter(path, obj, instance, instance_index, counter)
170        raise CommandExecutionError("Invalid counter specified: {}".format(path))
171
172    build_counter = staticmethod(build_counter)
173
174    def __init__(self, path, obj, instance, index, counter):
175        self.path = path
176        self.obj = obj
177        self.instance = instance
178        self.index = index
179        self.counter = counter
180        self.handle = None
181        self.info = None
182        self.type = None
183
184    def add_to_query(self, query):
185        """
186        Add the current path to the query
187
188        Args:
189            query (obj):
190                The handle to the query to add the counter
191        """
192        self.handle = win32pdh.AddCounter(query, self.path)
193
194    def get_info(self):
195        """
196        Get information about the counter
197
198        .. note::
199            GetCounterInfo sometimes crashes in the wrapper code. Fewer crashes
200            if this is called after sampling data.
201        """
202        if not self.info:
203            ci = win32pdh.GetCounterInfo(self.handle, 0)
204            self.info = {
205                "type": ci[0],
206                "version": ci[1],
207                "scale": ci[2],
208                "default_scale": ci[3],
209                "user_data": ci[4],
210                "query_user_data": ci[5],
211                "full_path": ci[6],
212                "machine_name": ci[7][0],
213                "object_name": ci[7][1],
214                "instance_name": ci[7][2],
215                "parent_instance": ci[7][3],
216                "instance_index": ci[7][4],
217                "counter_name": ci[7][5],
218                "explain_text": ci[8],
219            }
220        return self.info
221
222    def value(self):
223        """
224        Return the counter value
225
226        Returns:
227            long: The counter value
228        """
229        (counter_type, value) = win32pdh.GetFormattedCounterValue(
230            self.handle, win32pdh.PDH_FMT_DOUBLE
231        )
232        self.type = counter_type
233        return value
234
235    def type_string(self):
236        """
237        Returns the names of the flags that are set in the Type field
238
239        It can be used to format the counter.
240        """
241        type = self.get_info()["type"]
242        type_list = []
243        for member in dir(self):
244            if member.startswith("PERF_"):
245                bit = getattr(self, member)
246                if bit and bit & type:
247                    type_list.append(member[5:])
248        return type_list
249
250    def __str__(self):
251        return self.path
252
253
254def list_objects():
255    """
256    Get a list of available counter objects on the system
257
258    Returns:
259        list: A list of counter objects
260    """
261    return sorted(win32pdh.EnumObjects(None, None, -1, 0))
262
263
264def list_counters(obj):
265    """
266    Get a list of counters available for the object
267
268    Args:
269        obj (str):
270            The name of the counter object. You can get a list of valid names
271            using the ``list_objects`` function
272
273    Returns:
274        list: A list of counters available to the passed object
275    """
276    return win32pdh.EnumObjectItems(None, None, obj, -1, 0)[0]
277
278
279def list_instances(obj):
280    """
281    Get a list of instances available for the object
282
283    Args:
284        obj (str):
285            The name of the counter object. You can get a list of valid names
286            using the ``list_objects`` function
287
288    Returns:
289        list: A list of instances available to the passed object
290    """
291    return win32pdh.EnumObjectItems(None, None, obj, -1, 0)[1]
292
293
294def build_counter_list(counter_list):
295    r"""
296    Create a list of Counter objects to be used in the pdh query
297
298    Args:
299        counter_list (list):
300            A list of tuples containing counter information. Each tuple should
301            contain the object, instance, and counter name. For example, to
302            get the ``% Processor Time`` counter for all Processors on the
303            system (``\Processor(*)\% Processor Time``) you would pass a tuple
304            like this:
305
306            ```
307            counter_list = [('Processor', '*', '% Processor Time')]
308            ```
309
310            If there is no ``instance`` for the counter, pass ``None``
311
312            Multiple counters can be passed like so:
313
314            ```
315            counter_list = [('Processor', '*', '% Processor Time'),
316                            ('System', None, 'Context Switches/sec')]
317            ```
318
319            .. note::
320                Invalid counters are ignored
321
322    Returns:
323        list: A list of Counter objects
324    """
325    counters = []
326    index = 0
327    for obj, instance, counter_name in counter_list:
328        try:
329            counter = Counter.build_counter(obj, instance, index, counter_name)
330            index += 1
331            counters.append(counter)
332        except CommandExecutionError as exc:
333            # Not a valid counter
334            log.debug(exc.strerror)
335            continue
336    return counters
337
338
339def get_all_counters(obj, instance_list=None):
340    """
341    Get the values for all counters available to a Counter object
342
343    Args:
344
345        obj (str):
346            The name of the counter object. You can get a list of valid names
347            using the ``list_objects`` function
348
349        instance_list (list):
350            A list of instances to return. Use this to narrow down the counters
351            that are returned.
352
353            .. note::
354                ``_Total`` is returned as ``*``
355    """
356    counters, instances_avail = win32pdh.EnumObjectItems(None, None, obj, -1, 0)
357
358    if instance_list is None:
359        instance_list = instances_avail
360
361    if not isinstance(instance_list, list):
362        instance_list = [instance_list]
363
364    counter_list = []
365    for counter in counters:
366        for instance in instance_list:
367            instance = "*" if instance.lower() == "_total" else instance
368            counter_list.append((obj, instance, counter))
369        else:  # pylint: disable=useless-else-on-loop
370            counter_list.append((obj, None, counter))
371
372    return get_counters(counter_list) if counter_list else {}
373
374
375def get_counters(counter_list):
376    """
377    Get the values for the passes list of counters
378
379    Args:
380        counter_list (list):
381            A list of counters to lookup
382
383    Returns:
384        dict: A dictionary of counters and their values
385    """
386    if not isinstance(counter_list, list):
387        raise CommandExecutionError("counter_list must be a list of tuples")
388
389    try:
390        # Start a Query instances
391        query = win32pdh.OpenQuery()
392
393        # Build the counters
394        counters = build_counter_list(counter_list)
395
396        # Add counters to the Query
397        for counter in counters:
398            counter.add_to_query(query)
399
400        # https://docs.microsoft.com/en-us/windows/desktop/perfctrs/collecting-performance-data
401        win32pdh.CollectQueryData(query)
402        # The sleep here is required for counters that require more than 1
403        # reading
404        time.sleep(1)
405        win32pdh.CollectQueryData(query)
406        ret = {}
407
408        for counter in counters:
409            try:
410                ret.update({counter.path: counter.value()})
411            except pywintypes.error as exc:
412                if exc.strerror == "No data to return.":
413                    # Some counters are not active and will throw an error if
414                    # there is no data to return
415                    continue
416                else:
417                    raise
418
419    except pywintypes.error as exc:
420        if exc.strerror == "No data to return.":
421            # Sometimess, win32pdh.CollectQueryData can err
422            # so just ignore it
423            return {}
424        else:
425            raise
426
427    finally:
428        win32pdh.CloseQuery(query)
429
430    return ret
431
432
433def get_counter(obj, instance, counter):
434    """
435    Get the value of a single counter
436
437    Args:
438
439        obj (str):
440            The name of the counter object. You can get a list of valid names
441            using the ``list_objects`` function
442
443        instance (str):
444            The counter instance you wish to return. Get a list of instances
445            using the ``list_instances`` function
446
447            .. note::
448                ``_Total`` is returned as ``*``
449
450        counter (str):
451            The name of the counter. Get a list of counters using the
452            ``list_counters`` function
453    """
454    return get_counters([(obj, instance, counter)])
455