1# Copyright 2017 Christoph Reiter
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7
8"""
9Interface around faulthandler to save and restore segfault info for the
10next program invocation.
11"""
12
13import os
14import ctypes
15import errno
16import re
17import atexit
18
19import faulthandler
20
21from quodlibet.util.dprint import print_exc
22
23
24_fileobj = None
25
26
27class FaultHandlerCrash(Exception):
28    """The exception type used for raising errors with a faulthandler
29    stacktrace. Needed so we can add special handling in the error reporting
30    code paths.
31    """
32
33    def get_grouping_key(self):
34        """Given a stacktrace produced by the faulthandler module returns a
35        short string for grouping similar stacktraces together.
36
37        Args:
38            stacktrace (str)
39        Returns:
40            str
41        """
42
43        stacktrace = str(self)
44        if isinstance(stacktrace, bytes):
45            stacktrace = stacktrace.decode("utf-8", "replace")
46
47        assert isinstance(stacktrace, str)
48
49        # Extract the basename and the function name for each line and hash
50        # them. Could be smarter, but let's try this for now..
51        reg = re.compile(r'.*?"([^"]+).*?(\w+$)')
52        values = []
53        for l in stacktrace.splitlines():
54            m = reg.match(l)
55            if m is not None:
56                path, func = m.groups()
57                path = os.path.basename(path)
58                values.extend([path, func])
59        return u"|".join(values)
60
61
62def enable(path):
63    """Enable crash reporting and create empty target file
64
65    Args:
66        path (pathlike): the location of the crash log target path
67    Raises:
68        IOError: In case the location is not writable
69    """
70
71    global _fileobj
72
73    if _fileobj is not None:
74        raise Exception("already enabled")
75
76    # we open as reading so raise_and_clear_error() can extract the old error
77    try:
78        _fileobj = open(path, "rb+")
79    except IOError as e:
80        if e.errno == errno.ENOENT:
81            _fileobj = open(path, "wb+")
82        else:
83            raise
84
85    faulthandler.enable(_fileobj, all_threads=False)
86
87
88def disable():
89    """Disable crash reporting and removes the target file
90
91    Does not raise.
92    """
93
94    global _fileobj
95
96    if _fileobj is None:
97        return
98
99    faulthandler.disable()
100
101    try:
102        _fileobj.close()
103        os.unlink(_fileobj.name)
104    except (OSError, IOError):
105        pass
106    _fileobj = None
107
108
109@atexit.register
110def _at_exit():
111    disable()
112
113
114def raise_and_clear_error():
115    """Raises an error if there is one. Calling this will clear the error
116    so a second call won't do anything.
117
118    enable() needs to be called first.
119
120    Raises:
121        FaultHandlerCrash
122    """
123
124    global _fileobj
125
126    if _fileobj is None:
127        return
128
129    try:
130        _fileobj.seek(0)
131        text = _fileobj.read().decode("utf-8", "replace").strip()
132        _fileobj.seek(0)
133        _fileobj.truncate()
134    except IOError:
135        print_exc()
136    else:
137        if text:
138            raise FaultHandlerCrash(text)
139
140
141def crash():
142    """Makes the process segfault. For testing purposes"""
143
144    if os.name == "nt":
145        i = ctypes.c_char(b'a')
146        j = ctypes.pointer(i)
147        c = 0
148        while True:
149            j[c] = b'a'
150            c += 1
151    else:
152        ctypes.string_at(0)
153