1"""
2Print a stacktrace when sent a SIGUSR1 for debugging
3"""
4
5import inspect
6import os
7import signal
8import sys
9import tempfile
10import time
11import traceback
12
13import salt.utils.files
14import salt.utils.stringutils
15
16
17def _makepretty(printout, stack):
18    """
19    Pretty print the stack trace and environment information
20    for debugging those hard to reproduce user problems.  :)
21    """
22    printout.write("======== Salt Debug Stack Trace =========\n")
23    traceback.print_stack(stack, file=printout)
24    printout.write("=========================================\n")
25
26
27def _handle_sigusr1(sig, stack):
28    """
29    Signal handler for SIGUSR1, only available on Unix-like systems
30    """
31    # When running in the foreground, do the right  thing
32    # and spit out the debug info straight to the console
33    if sys.stderr.isatty():
34        output = sys.stderr
35        _makepretty(output, stack)
36    else:
37        filename = "salt-debug-{}.log".format(int(time.time()))
38        destfile = os.path.join(tempfile.gettempdir(), filename)
39        with salt.utils.files.fopen(destfile, "w") as output:
40            _makepretty(output, stack)
41
42
43def _handle_sigusr2(sig, stack):
44    """
45    Signal handler for SIGUSR2, only available on Unix-like systems
46    """
47    try:
48        import yappi
49    except ImportError:
50        return
51    if yappi.is_running():
52        yappi.stop()
53        filename = "callgrind.salt-{}-{}".format(int(time.time()), os.getpid())
54        destfile = os.path.join(tempfile.gettempdir(), filename)
55        yappi.get_func_stats().save(destfile, type="CALLGRIND")
56        if sys.stderr.isatty():
57            sys.stderr.write("Saved profiling data to: {}\n".format(destfile))
58        yappi.clear_stats()
59    else:
60        if sys.stderr.isatty():
61            sys.stderr.write("Profiling started\n")
62        yappi.start()
63
64
65def enable_sig_handler(signal_name, handler):
66    """
67    Add signal handler for signal name if it exists on given platform
68    """
69    if hasattr(signal, signal_name):
70        signal.signal(getattr(signal, signal_name), handler)
71
72
73def enable_sigusr1_handler():
74    """
75    Pretty print a stack trace to the console or a debug log under /tmp
76    when any of the salt daemons such as salt-master are sent a SIGUSR1
77    """
78    enable_sig_handler("SIGUSR1", _handle_sigusr1)
79    # Also canonical BSD-way of printing progress is SIGINFO
80    # which on BSD-derivatives can be sent via Ctrl+T
81    enable_sig_handler("SIGINFO", _handle_sigusr1)
82
83
84def enable_sigusr2_handler():
85    """
86    Toggle YAPPI profiler
87    """
88    enable_sig_handler("SIGUSR2", _handle_sigusr2)
89
90
91def inspect_stack():
92    """
93    Return a string of which function we are currently in.
94    """
95    return {"co_name": inspect.stack()[1][3]}
96
97
98def caller_name(skip=2, include_lineno=False):
99    """
100    Get a name of a caller in the format module.class.method
101
102    `skip` specifies how many levels of stack to skip while getting caller
103    name. skip=1 means "who calls me", skip=2 "who calls my caller" etc.
104
105    An empty string is returned if skipped levels exceed stack height
106
107    Source: https://gist.github.com/techtonik/2151727
108    """
109    stack = inspect.stack()
110    start = 0 + skip
111    if len(stack) < start + 1:
112        return ""
113    parentframe = stack[start][0]
114
115    name = []
116    if include_lineno is True:
117        try:
118            lineno = inspect.getframeinfo(parentframe).lineno
119        except:  # pylint: disable=bare-except
120            lineno = None
121    module = inspect.getmodule(parentframe)
122    # `modname` can be None when frame is executed directly in console
123    # TODO(techtonik): consider using __main__
124    if module:
125        name.append(module.__name__)
126    # detect classname
127    if "self" in parentframe.f_locals:
128        # I don't know any way to detect call from the object method
129        # XXX: there seems to be no way to detect static method call - it will
130        #      be just a function call
131        name.append(parentframe.f_locals["self"].__class__.__name__)
132    codename = parentframe.f_code.co_name
133    if codename != "<module>":  # top level usually
134        name.append(codename)  # function or a method
135    del parentframe
136    fullname = ".".join(name)
137    if include_lineno and lineno:
138        fullname += ":{}".format(lineno)
139    return fullname
140