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