1# -*- coding: utf-8 -*- 2 3# Copyright (C) 2014-2015 - Garrett Regier 4# 5# libpeas is free software; you can redistribute it and/or 6# modify it under the terms of the GNU Lesser General Public 7# License as published by the Free Software Foundation; either 8# version 2.1 of the License, or (at your option) any later version. 9# 10# libpeas is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13# Lesser General Public License for more details. 14# 15# You should have received a copy of the GNU Lesser General Public 16# License along with this library; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 19import gc 20import gettext 21import importlib 22import os 23import signal 24import sys 25import traceback 26 27from gi.repository import GLib, GObject 28 29 30# Derive from something not normally caught 31class FailedError(BaseException): 32 pass 33 34 35class Hooks(object): 36 def __init__(self): 37 if not ALREADY_INITIALIZED: 38 int_handler = signal.getsignal(signal.SIGINT) 39 40 # Use the default handler instead of raising KeyboardInterrupt 41 if int_handler == signal.default_int_handler: 42 signal.signal(signal.SIGINT, signal.SIG_DFL) 43 44 sys.argv = [PRGNAME] 45 46 gettext.install(GETTEXT_PACKAGE, PEAS_LOCALEDIR) 47 48 self.__module_cache = {} 49 self.__extension_cache = {} 50 51 @staticmethod 52 def failed(): 53 # This is implemented by the plugin loader 54 raise NotImplementedError('Hooks.failed()') 55 56 @staticmethod 57 def format_plugin_exception(): 58 formatted = traceback.format_exception(*sys.exc_info()) 59 60 # Remove all mentions of this file 61 for i in range(len(formatted)): 62 if __file__ in formatted[i]: 63 while not formatted[i].startswith('Traceback'): 64 formatted[i] = '' 65 i -= 1 66 67 return ''.join(formatted) 68 69 def call(self, name, args, return_type): 70 try: 71 result = getattr(self, name)(*args) 72 73 except FailedError: 74 raise 75 76 except: 77 self.failed("Failed to run internal Python hook '%s':\n%s" % 78 (name, traceback.format_exc())) 79 80 # We always allow None 81 if result is not None and not isinstance(result, return_type): 82 self.failed("Failed to run internal Python hook '%s': " 83 "expected %s, got %s" % 84 (name, return_type, result)) 85 86 return result 87 88 def load(self, filename, module_dir, module_name): 89 try: 90 return self.__module_cache[filename] 91 92 except KeyError: 93 pass 94 95 if module_name in sys.modules: 96 self.__module_cache[filename] = None 97 self.failed("Error loading plugin '%s': " 98 "module name '%s' has already been used" % 99 (filename, module_name)) 100 101 if module_dir not in sys.path: 102 sys.path.insert(0, module_dir) 103 104 try: 105 module = importlib.import_module(module_name) 106 107 except: 108 module = None 109 self.failed("Error importing plugin '%s':\n%s" % 110 (module_name, self.format_plugin_exception())) 111 112 else: 113 self.__extension_cache[module] = {} 114 115 finally: 116 self.__module_cache[filename] = module 117 118 return module 119 120 def find_extension_type(self, gtype, module): 121 module_gtypes = self.__extension_cache[module] 122 123 try: 124 return module_gtypes[gtype] 125 126 except KeyError: 127 pass 128 129 for key in getattr(module, '__all__', module.__dict__): 130 value = getattr(module, key) 131 132 try: 133 value_gtype = value.__gtype__ 134 135 except AttributeError: 136 continue 137 138 if GObject.type_is_a(value_gtype, gtype): 139 module_gtypes[gtype] = value 140 return value 141 142 module_gtypes[gtype] = None 143 return None 144 145 def garbage_collect(self): 146 gc.collect() 147 148 def all_plugins_unloaded(self): 149 pass 150 151 def exit(self): 152 gc.collect() 153 154 155if os.getenv('PEAS_PYTHON_PROFILE') is not None: 156 import cProfile 157 import pstats 158 import threading 159 import weakref 160 161 162 class Hooks(Hooks): 163 def __init__(self): 164 super(Hooks, self).__init__() 165 166 sort = os.getenv('PEAS_PYTHON_PROFILE', default='time') 167 self.__stat_sort = sort.split(';') 168 169 self.__stats = None 170 self.__stats_lock = threading.Lock() 171 172 self.__thread_refs = [] 173 self.__thread_local = threading.local() 174 175 threading.setprofile(self.__init_thread) 176 177 self.__profile = cProfile.Profile() 178 self.__profile.enable() 179 180 def __add_stats(self, profile): 181 profile.disable() 182 183 with self.__stats_lock: 184 if self.__stats is None: 185 self.__stats = pstats.Stats(profile) 186 187 else: 188 self.__stats.add(profile) 189 190 def __init_thread(self, *unused): 191 # Only call once per thread 192 sys.setprofile(None) 193 194 thread_profile = cProfile.Profile() 195 196 def thread_finished(thread_ref): 197 self.__add_stats(thread_profile) 198 199 self.__thread_refs.remove(thread_ref) 200 201 # Need something to weakref, the 202 # current thread does not support it 203 thread_ref = set() 204 self.__thread_local.ref = thread_ref 205 206 self.__thread_refs.append(weakref.ref(thread_ref, 207 thread_finished)) 208 209 # Only enable the profile at the end 210 thread_profile.enable() 211 212 def all_plugins_unloaded(self): 213 super(Hooks, self).all_plugins_unloaded() 214 215 self.__add_stats(self.__profile) 216 217 with self.__stats_lock: 218 stats = self.__stats.strip_dirs() 219 stats.sort_stats(*self.__stat_sort) 220 stats.print_stats() 221 222 # Need to create a new profile to avoid adding the stats twice 223 self.__profile = cProfile.Profile() 224 self.__profile.enable() 225 226 def exit(self): 227 super(Hooks, self).exit() 228 229 self.__profile.disable() 230 231 232hooks = Hooks() 233 234# ex:ts=4:et: 235