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