1# Copyright (C) 2010, 2013 Red Hat, Inc.
2# Copyright (C) 2010 Cole Robinson <crobinso@redhat.com>
3#
4# This work is licensed under the GNU GPLv2 or later.
5# See the COPYING file in the top-level directory.
6
7import os
8import sys
9import threading
10import traceback
11import types
12
13from gi.repository import Gdk
14from gi.repository import GLib
15from gi.repository import GObject
16from gi.repository import Gtk
17
18from virtinst import log
19from virtinst import xmlutil
20
21from . import config
22
23
24class vmmGObject(GObject.GObject):
25    # Objects can set this to false to disable leak tracking
26    _leak_check = True
27
28    # Singleton reference, if applicable (vmmSystray, vmmInspection, ...)
29    _instance = None
30
31    # windowlist mapping, if applicable (vmmVMWindow, vmmHost, ...)
32    _instances = None
33
34    # This saves a bunch of imports and typing
35    RUN_FIRST = GObject.SignalFlags.RUN_FIRST
36
37    @staticmethod
38    def idle_add(func, *args, **kwargs):
39        """
40        Make sure idle functions are run thread safe
41        """
42        def cb():
43            return func(*args, **kwargs)
44        return GLib.idle_add(cb)
45
46    def __init__(self):
47        GObject.GObject.__init__(self)
48
49        self.__cleaned_up = False
50
51        self._gobject_handles = []
52        self._gobject_handles_map = {}
53        self._gobject_timeouts = []
54        self._gsettings_handles = []
55
56        self._signal_id_map = {}
57        self._next_signal_id = 1
58
59        self.object_key = str(self)
60
61        # Config might not be available if we error early in startup
62        if config.vmmConfig.is_initialized() and self._leak_check:
63            self.config.add_object(self.object_key)
64
65    def _get_err(self):
66        from . import error
67        return error.vmmErrorDialog.get_instance()
68    err = property(_get_err)
69
70    def cleanup(self):
71        if self.__cleaned_up:
72            return  # pragma: no cover
73
74        # Do any cleanup required to drop reference counts so object is
75        # actually reaped by python. Usually means unregistering callbacks
76        try:
77            # pylint: disable=protected-access
78            if self.__class__._instance == self:
79                # We set this to True which can help us catch instances
80                # where cleanup routines try to reinit singleton classes
81                self.__class__._instance = True
82
83            _instances = self.__class__._instances or {}
84            for k, v in list(_instances.items()):
85                if v == self:
86                    _instances.pop(k)
87
88            self._cleanup()
89
90            for h in self._gsettings_handles[:]:
91                self.remove_gsettings_handle(h)
92            for h in self._gobject_handles[:]:
93                if GObject.GObject.handler_is_connected(self, h):
94                    self.disconnect(h)
95            for h in self._gobject_timeouts[:]:
96                self.remove_gobject_timeout(h)
97        except Exception:  # pragma: no cover
98            log.exception("Error cleaning up %s", self)
99
100        self.__cleaned_up = True
101
102    def _cleanup_on_app_close(self):
103        from .engine import vmmEngine
104        vmmEngine.get_instance().connect(
105                "app-closing", lambda src: self.cleanup())
106
107    def _cleanup(self):
108        raise NotImplementedError("_cleanup must be implemented in subclass")
109
110    def __del__(self):
111        try:
112            if config.vmmConfig.is_initialized() and self._leak_check:
113                self.config.remove_object(self.object_key)
114        except Exception:  # pragma: no cover
115            log.exception("Error removing %s", self.object_key)
116
117    @property
118    def config(self):
119        return config.vmmConfig.get_instance()
120
121
122    # pylint: disable=arguments-differ
123    # Newer pylint can detect, but warns that overridden arguments are wrong
124
125    def connect(self, name, callback, *args):
126        """
127        GObject connect() wrapper to simplify callers, and track handles
128        for easy cleanup
129        """
130        ret = GObject.GObject.connect(self, name, callback, *args)
131
132        # If the passed callback is a method of a class instance,
133        # keep a mapping of id(instance):[handles]. This lets us
134        # implement disconnect_by_obj to simplify signal removal
135        if isinstance(callback, types.MethodType):
136            i = id(callback.__self__)
137            if i not in self._gobject_handles_map:
138                self._gobject_handles_map[i] = []
139            self._gobject_handles_map[i].append(ret)
140
141        self._gobject_handles.append(ret)
142        return ret
143
144    def disconnect(self, handle):
145        """
146        GObject disconnect() wrapper to simplify callers
147        """
148        ret = GObject.GObject.disconnect(self, handle)
149        self._gobject_handles.remove(handle)
150        return ret
151
152    def disconnect_by_obj(self, instance):
153        """
154        disconnect() every signal attached to a method of the passed instance
155        """
156        i = id(instance)
157        for handle in self._gobject_handles_map.get(i, []):
158            if handle in self._gobject_handles:
159                self.disconnect(handle)
160        self._gobject_handles_map.pop(i, None)
161
162    def timeout_add(self, timeout, func, *args):
163        """
164        GLib timeout_add wrapper to simplify callers, and track handles
165        for easy cleanup
166        """
167        ret = GLib.timeout_add(timeout, func, *args)
168        self.add_gobject_timeout(ret)
169        return ret
170
171    def emit(self, signal_name, *args):
172        """
173        GObject emit() wrapper to simplify callers
174        """
175        if not self._is_main_thread():  # pragma: no cover
176            log.error("emitting signal from non-main thread. This is a bug "
177                    "please report it. thread=%s self=%s signal=%s",
178                    self._thread_name(), self, signal_name)
179        return GObject.GObject.emit(self, signal_name, *args)
180
181    def add_gsettings_handle(self, handle):
182        self._gsettings_handles.append(handle)
183    def remove_gsettings_handle(self, handle):
184        self.config.remove_notifier(handle)
185        self._gsettings_handles.remove(handle)
186
187    def add_gobject_timeout(self, handle):
188        self._gobject_timeouts.append(handle)
189    def remove_gobject_timeout(self, handle):
190        GLib.source_remove(handle)
191        self._gobject_timeouts.remove(handle)
192
193    def _start_thread(self, target=None, name=None, args=None, kwargs=None):
194        # Helper for starting a daemonized thread
195        t = threading.Thread(target=target, name=name,
196            args=args or [], kwargs=kwargs or {})
197        t.daemon = True
198        t.start()
199
200
201    ##############################
202    # Internal debugging helpers #
203    ##############################
204
205    def _refcount(self):
206        return sys.getrefcount(self)
207
208    def _logtrace(self, msg=""):
209        if msg:
210            msg += " "
211        log.debug("%s(%s %s)\n:%s",
212                      msg, self.object_key, self._refcount(),
213                       "".join(traceback.format_stack()))
214
215    def _gc_get_referrers(self):  # pragma: no cover
216        import gc
217        import pprint
218        pprint.pprint(gc.get_referrers(self))
219
220    def _thread_name(self):
221        return threading.current_thread().name
222
223    def _is_main_thread(self):
224        return self._thread_name() == "MainThread"
225
226
227    ##############################
228    # Custom signal/idle helpers #
229    ##############################
230
231    def connect_once(self, signal, func, *args):
232        """
233        Like standard glib connect(), but only runs the signal handler
234        once, then unregisters it
235        """
236        id_list = []
237
238        def wrap_func(*wrapargs):
239            if id_list:
240                self.disconnect(id_list[0])
241
242            return func(*wrapargs)
243
244        conn_id = self.connect(signal, wrap_func, *args)
245        id_list.append(conn_id)
246
247        return conn_id
248
249    def connect_opt_out(self, signal, func, *args):
250        """
251        Like standard glib connect(), but allows the signal handler to
252        unregister itself if it returns True
253        """
254        id_list = []
255
256        def wrap_func(*wrapargs):
257            ret = func(*wrapargs)
258            if ret and id_list:
259                self.disconnect(id_list[0])
260
261        conn_id = self.connect(signal, wrap_func, *args)
262        id_list.append(conn_id)
263
264        return conn_id
265
266    def idle_emit(self, signal, *args):
267        """
268        Safe wrapper for using 'self.emit' with GLib.idle_add
269        """
270        def emitwrap(_s, *_a):
271            self.emit(_s, *_a)
272            return False
273
274        self.idle_add(emitwrap, signal, *args)
275
276
277class vmmGObjectUI(vmmGObject):
278    def __init__(self, filename, windowname, builder=None, topwin=None):
279        vmmGObject.__init__(self)
280        self._external_topwin = bool(topwin)
281        self.__cleaned_up = False
282
283        if filename:
284            uifile = os.path.join(self.config.get_ui_dir(), filename)
285
286            self.builder = Gtk.Builder()
287            self.builder.set_translation_domain("virt-manager")
288            self.builder.add_from_file(uifile)
289
290            if not topwin:
291                self.topwin = self.widget(windowname)
292                self.topwin.hide()
293            else:
294                self.topwin = topwin
295        else:
296            self.builder = builder
297            self.topwin = topwin
298
299        self._err = None
300
301    def _get_err(self):
302        if self._err is None:
303            from . import error
304            self._err = error.vmmErrorDialog(self.topwin)
305        return self._err
306    err = property(_get_err)
307
308    def widget(self, name):
309        ret = self.builder.get_object(name)
310        if not ret:
311            raise xmlutil.DevError("Did not find widget name=%s" % name)
312        return ret
313
314    def cleanup(self):
315        if self.__cleaned_up:
316            return  # pragma: no cover
317
318        try:
319            self.close()
320            vmmGObject.cleanup(self)
321            self.builder = None
322            if not self._external_topwin:
323                self.topwin.destroy()
324            self.topwin = None
325            self._err = None
326        except Exception:  # pragma: no cover
327            log.exception("Error cleaning up %s", self)
328
329        self.__cleaned_up = True
330
331    def _cleanup(self):
332        raise NotImplementedError("_cleanup must be implemented in subclass")
333
334    def close(self, ignore1=None, ignore2=None):
335        pass
336
337    def is_visible(self):
338        return bool(self.topwin and self.topwin.get_visible())
339
340    def bind_escape_key_close(self):
341        def close_on_escape(src_ignore, event):
342            if Gdk.keyval_name(event.keyval) == "Escape":
343                self.close()
344        self.topwin.connect("key-press-event", close_on_escape)
345
346    def _set_cursor(self, cursor_type):
347        gdk_window = self.topwin.get_window()
348        if not gdk_window:
349            return
350
351        try:
352            cursor = Gdk.Cursor.new_from_name(
353                    gdk_window.get_display(), cursor_type)
354            gdk_window.set_cursor(cursor)
355        except Exception:  # pragma: no cover
356            # If a cursor icon theme isn't installed this can cause errors
357            # https://bugzilla.redhat.com/show_bug.cgi?id=1516588
358            log.debug("Error setting cursor_type=%s",
359                    cursor_type, exc_info=True)
360
361    def set_finish_cursor(self):
362        self.topwin.set_sensitive(False)
363        self._set_cursor("progress")
364
365    def reset_finish_cursor(self, topwin=None):
366        if not topwin:
367            topwin = self.topwin
368        topwin.set_sensitive(True)
369        self._set_cursor("default")
370
371    def _cleanup_on_conn_removed(self):
372        from .connmanager import vmmConnectionManager
373        connmanager = vmmConnectionManager.get_instance()
374
375        def _cb(_src, uri):
376            _conn = getattr(self, "conn", None)
377            if _conn and _conn.get_uri() == uri:
378                self.cleanup()
379                return True
380        connmanager.connect_opt_out("conn-removed", _cb)
381