This commit is contained in:
Akkariin Meiko
2022-03-12 03:16:09 +08:00
Unverified
parent 12b76e0c7a
commit 27c4ec74a1
10075 changed files with 5122287 additions and 1 deletions
@@ -0,0 +1,74 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Development Utilities
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Development Utilities Common Data module."""
import gi
from gi.repository import GObject
class Dispatcher (object):
def __call__(self, iterator):
raise NotImplementedError("derived classes must override this method")
def cancel(self):
pass
class DefaultDispatcher (Dispatcher):
def __call__(self, iterator):
for x in iterator:
pass
class GSourceDispatcher (Dispatcher):
def __init__(self):
Dispatcher.__init__(self)
self.source_id = None
def __call__(self, iterator):
if self.source_id is not None:
GObject.source_remove(self.source_id)
def iteration():
r = iterator.__next__()
if not r:
self.source_id = None
return r
self.source_id = GObject.idle_add(
iteration, priority=GObject.PRIORITY_LOW)
def cancel(self):
if self.source_id is None:
return
GObject.source_remove(self.source_id)
self.source_id = None
@@ -0,0 +1,528 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Development Utilities
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Development Utilities Common GUI module."""
import os
import logging
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
from gi.types import GObjectMeta
import GstDebugViewer
from GstDebugViewer.Common import utils
from .generictreemodel import GenericTreeModel
def widget_add_popup_menu(widget, menu, button=3):
def popup_callback(widget, event):
if event.button == button:
menu.popup(
None, None, None, None, event.button, event.get_time())
return False
widget.connect("button-press-event", popup_callback)
class Actions (dict):
def __init__(self):
dict.__init__(self)
self.groups = {}
def __getattr__(self, name):
try:
return self[name]
except KeyError:
if "_" in name:
try:
return self[name.replace("_", "-")]
except KeyError:
pass
raise AttributeError("no action with name %r" % (name,))
def add_group(self, group):
name = group.props.name
if name in self.groups:
raise ValueError("already have a group named %s", name)
self.groups[name] = group
for action in group.list_actions():
self[action.props.name] = action
class Widgets (dict):
def __init__(self, builder):
widgets = (obj for obj in builder.get_objects()
if isinstance(obj, Gtk.Buildable))
# Gtk.Widget.get_name() shadows out the GtkBuildable interface method
# of the same name, hence calling the unbound interface method here:
items = ((Gtk.Buildable.get_name(w), w,) for w in widgets)
dict.__init__(self, items)
def __getattr__(self, name):
try:
return self[name]
except KeyError:
if "_" in name:
try:
return self[name.replace("_", "-")]
except KeyError:
pass
raise AttributeError("no widget with name %r" % (name,))
class WidgetFactory (object):
def __init__(self, directory):
self.directory = directory
def get_builder(self, filename):
builder_filename = os.path.join(self.directory, filename)
builder = Gtk.Builder()
builder.set_translation_domain(GstDebugViewer.GETTEXT_DOMAIN)
builder.add_from_file(builder_filename)
return builder
def make(self, filename, widget_name, autoconnect=None):
builder = self.get_builder(filename)
if autoconnect is not None:
builder.connect_signals(autoconnect)
return Widgets(builder)
def make_one(self, filename, widget_name):
builder = self.get_builder(filename)
return builder.get_object(widget_name)
class UIFactory (object):
def __init__(self, ui_filename, actions=None):
self.filename = ui_filename
if actions:
self.action_groups = actions.groups
else:
self.action_groups = ()
def make(self, extra_actions=None):
ui_manager = Gtk.UIManager()
for action_group in list(self.action_groups.values()):
ui_manager.insert_action_group(action_group, 0)
if extra_actions:
for action_group in extra_actions.groups:
ui_manager.insert_action_group(action_group, 0)
ui_manager.add_ui_from_file(self.filename)
ui_manager.ensure_update()
return ui_manager
class MetaModel (GObjectMeta):
"""Meta class for easy setup of gtk tree models.
Looks for a class attribute named `columns' which must be set to a
sequence of the form name1, type1, name2, type2, ..., where the
names are strings. This metaclass adds the following attributes
to created classes:
cls.column_types = (type1, type2, ...)
cls.column_ids = (0, 1, ...)
cls.name1 = 0
cls.name2 = 1
...
Example: A Gtk.ListStore derived model can use
columns = ("COL_NAME", str, "COL_VALUE", str)
and use this in __init__:
GObject.GObject.__init__ (self, *self.column_types)
Then insert data like this:
self.set (self.append (),
self.COL_NAME, "spam",
self.COL_VALUE, "ham")
"""
def __init__(cls, name, bases, dict):
super(MetaModel, cls).__init__(name, bases, dict)
spec = tuple(cls.columns)
column_names = spec[::2]
column_types = spec[1::2]
column_indices = list(range(len(column_names)))
for col_index, col_name, in zip(column_indices, column_names):
setattr(cls, col_name, col_index)
cls.column_types = column_types
cls.column_ids = tuple(column_indices)
class Manager (object):
"""GUI Manager base class."""
@classmethod
def iter_item_classes(cls):
msg = "%s class does not support manager item class access"
raise NotImplementedError(msg % (cls.__name__,))
@classmethod
def find_item_class(self, **kw):
return self.__find_by_attrs(self.iter_item_classes(), kw)
def iter_items(self):
msg = "%s object does not support manager item access"
raise NotImplementedError(msg % (type(self).__name__,))
def find_item(self, **kw):
return self.__find_by_attrs(self.iter_items(), kw)
@staticmethod
def __find_by_attrs(i, kw):
from operator import attrgetter
if len(kw) != 1:
raise ValueError("need exactly one keyword argument")
attr, value = list(kw.items())[0]
getter = attrgetter(attr)
for item in i:
if getter(item) == value:
return item
else:
raise KeyError("no item such that item.%s == %r" % (attr, value,))
class StateString (object):
"""Descriptor for binding to StateSection classes."""
def __init__(self, option, default=None):
self.option = option
self.default = default
def __get__(self, section, section_class=None):
import configparser
if section is None:
return self
try:
return self.get(section)
except (configparser.NoSectionError,
configparser.NoOptionError,):
return self.get_default(section)
def __set__(self, section, value):
import configparser
self.set(section, value)
def get(self, section):
return section.get(self)
def get_default(self, section):
return self.default
def set(self, section, value):
if value is None:
value = ""
section.set(self, str(value))
class StateBool (StateString):
"""Descriptor for binding to StateSection classes."""
def get(self, section):
return section.state._parser.getboolean(section._name, self.option)
class StateInt (StateString):
"""Descriptor for binding to StateSection classes."""
def get(self, section):
return section.state._parser.getint(section._name, self.option)
class StateInt4 (StateString):
"""Descriptor for binding to StateSection classes. This implements storing
a tuple of 4 integers."""
def get(self, section):
value = StateString.get(self, section)
try:
l = value.split(",")
if len(l) != 4:
return None
else:
return tuple((int(v) for v in l))
except (AttributeError, TypeError, ValueError,):
return None
def set(self, section, value):
if value is None:
svalue = ""
elif len(value) != 4:
raise ValueError("value needs to be a 4-sequence, or None")
else:
svalue = ", ".join((str(v) for v in value))
return StateString.set(self, section, svalue)
class StateItem (StateString):
"""Descriptor for binding to StateSection classes. This implements storing
a class controlled by a Manager class."""
def __init__(self, option, manager_class, default=None):
StateString.__init__(self, option, default=default)
self.manager = manager_class
def get(self, section):
value = SectionString.get(self, section)
if not value:
return None
return self.parse_item(value)
def set(self, section, value):
if value is None:
svalue = ""
else:
svalue = value.name
StateString.set(self, section, svalue)
def parse_item(self, value):
name = value.strip()
try:
return self.manager.find_item_class(name=name)
except KeyError:
return None
class StateItemList (StateItem):
"""Descriptor for binding to StateSection classes. This implements storing
an ordered set of Manager items."""
def get(self, section):
value = StateString.get(self, section)
if not value:
return []
classes = []
for name in value.split(","):
item_class = self.parse_item(name)
if item_class is None:
continue
if not item_class in classes:
classes.append(item_class)
return classes
def get_default(self, section):
default = StateItem.get_default(self, section)
if default is None:
return []
else:
return default
def set(self, section, value):
if value is None:
svalue = ""
else:
svalue = ", ".join((v.name for v in value))
StateString.set(self, section, svalue)
class StateSection (object):
_name = None
def __init__(self, state):
self.state = state
if self._name is None:
raise NotImplementedError(
"subclasses must override the _name attribute")
def get(self, state_string):
return self.state._parser.get(self._name, state_string.option)
def set(self, state_string, value):
import configparser
parser = self.state._parser
try:
parser.set(self._name, state_string.option, value)
except configparser.NoSectionError:
parser.add_section(self._name)
parser.set(self._name, state_string.option, value)
class State (object):
def __init__(self, filename, old_filenames=()):
import configparser
self.sections = {}
self._filename = filename
self._parser = configparser.RawConfigParser()
success = self._parser.read([filename])
if not success:
for old_filename in old_filenames:
success = self._parser.read([old_filename])
if success:
break
def add_section_class(self, section_class):
self.sections[section_class._name] = section_class(self)
def save(self):
with utils.SaveWriteFile(self._filename, "wt") as fp:
self._parser.write(fp)
class WindowState (object):
def __init__(self):
self.logger = logging.getLogger("ui.window-state")
self.is_maximized = False
def attach(self, window, state):
self.window = window
self.state = state
self.window.connect("window-state-event",
self.handle_window_state_event)
geometry = self.state.geometry
if geometry:
self.window.move(*geometry[:2])
self.window.set_default_size(*geometry[2:])
if self.state.maximized:
self.logger.debug("initially maximized")
self.window.maximize()
def detach(self):
window = self.window
self.state.maximized = self.is_maximized
if not self.is_maximized:
position = tuple(window.get_position())
size = tuple(window.get_size())
self.state.geometry = position + size
self.window.disconnect_by_func(self.handle_window_state_event)
self.window = None
def handle_window_state_event(self, window, event):
if not event.changed_mask & Gdk.WindowState.MAXIMIZED:
return
if event.new_window_state & Gdk.WindowState.MAXIMIZED:
self.logger.debug("maximized")
self.is_maximized = True
else:
self.logger.debug("unmaximized")
self.is_maximized = False
@@ -0,0 +1,366 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Development Utilities
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Development Utilities Common Main module."""
import sys
import os
import traceback
from operator import attrgetter
import logging
import locale
import gettext
from gettext import gettext as _, ngettext
import gi
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gtk
class ExceptionHandler (object):
exc_types = (Exception,)
priority = 50
inherit_fork = True
_handling_exception = False
def __call__(self, exc_type, exc_value, exc_traceback):
raise NotImplementedError(
"derived classes need to override this method")
class DefaultExceptionHandler (ExceptionHandler):
exc_types = (BaseException,)
priority = 0
inherit_fork = True
def __init__(self, excepthook):
ExceptionHandler.__init__(self)
self.excepthook = excepthook
def __call__(self, *exc_info):
return self.excepthook(*exc_info)
class ExitOnInterruptExceptionHandler (ExceptionHandler):
exc_types = (KeyboardInterrupt,)
priority = 100
inherit_fork = False
exit_status = 2
def __call__(self, *args):
print("Interrupt caught, exiting.", file=sys.stderr)
sys.exit(self.exit_status)
class MainLoopWrapper (ExceptionHandler):
priority = 95
inherit_fork = False
def __init__(self, enter, exit):
ExceptionHandler.__init__(self)
self.exc_info = (None,) * 3
self.enter = enter
self.exit = exit
def __call__(self, *exc_info):
self.exc_info = exc_info
self.exit()
def run(self):
ExceptHookManager.register_handler(self)
try:
self.enter()
finally:
ExceptHookManager.unregister_handler(self)
if self.exc_info != (None,) * 3:
# Re-raise unhandled exception that occured while running the loop.
exc_type, exc_value, exc_tb = self.exc_info
raise exc_value
class ExceptHookManagerClass (object):
def __init__(self):
self._in_forked_child = False
self.handlers = []
def setup(self):
if sys.excepthook == self.__excepthook:
raise ValueError("already set up")
hook = sys.excepthook
self.__instrument_excepthook()
self.__instrument_fork()
self.register_handler(DefaultExceptionHandler(hook))
def shutdown(self):
if sys.excepthook != self.__excepthook:
raise ValueError("not set up")
self.__restore_excepthook()
self.__restore_fork()
def __instrument_excepthook(self):
hook = sys.excepthook
self._original_excepthook = hook
sys.excepthook = self.__excepthook
def __restore_excepthook(self):
sys.excepthook = self._original_excepthook
def __instrument_fork(self):
try:
fork = os.fork
except AttributeError:
# System has no fork() system call.
self._original_fork = None
else:
self._original_fork = fork
os.fork = self.__fork
def __restore_fork(self):
if not hasattr(os, "fork"):
return
os.fork = self._original_fork
def entered_forked_child(self):
self._in_forked_child = True
for handler in tuple(self.handlers):
if not handler.inherit_fork:
self.handlers.remove(handler)
def register_handler(self, handler):
if self._in_forked_child and not handler.inherit_fork:
return
self.handlers.append(handler)
def unregister_handler(self, handler):
self.handlers.remove(handler)
def __fork(self):
pid = self._original_fork()
if pid == 0:
# Child process.
self.entered_forked_child()
return pid
def __excepthook(self, exc_type, exc_value, exc_traceback):
for handler in sorted(self.handlers,
key=attrgetter("priority"),
reverse=True):
if handler._handling_exception:
continue
for type_ in handler.exc_types:
if issubclass(exc_type, type_):
break
else:
continue
handler._handling_exception = True
handler(exc_type, exc_value, exc_traceback)
# Not using try...finally on purpose here. If the handler itself
# fails with an exception, this prevents recursing into it again.
handler._handling_exception = False
return
else:
from warnings import warn
warn("ExceptHookManager: unhandled %r" % (exc_value,),
RuntimeWarning,
stacklevel=2)
ExceptHookManager = ExceptHookManagerClass()
class PathsBase (object):
data_dir = None
icon_dir = None
locale_dir = None
@classmethod
def setup_installed(cls, data_prefix):
"""Set up paths for running from a regular installation."""
pass
@classmethod
def setup_devenv(cls, source_dir):
"""Set up paths for running the development environment
(i.e. directly from the source dist)."""
pass
@classmethod
def ensure_setup(cls):
"""If paths are still not set up, try to set from a fallback."""
if cls.data_dir is None:
source_dir = os.path.dirname(
os.path.dirname(os.path.abspath(__file__)))
cls.setup_devenv(source_dir)
def __new__(cls):
raise RuntimeError("do not create instances of this class -- "
"use the class object directly")
class PathsProgramBase (PathsBase):
program_name = None
@classmethod
def setup_installed(cls, data_prefix):
if cls.program_name is None:
raise NotImplementedError(
"derived classes need to set program_name attribute")
cls.data_dir = os.path.join(data_prefix, cls.program_name)
cls.icon_dir = os.path.join(data_prefix, "icons")
cls.locale_dir = os.path.join(data_prefix, "locale")
@classmethod
def setup_devenv(cls, source_dir):
"""Set up paths for running the development environment
(i.e. directly from the source dist)."""
# This is essential: The GUI module needs to find the .glade file.
cls.data_dir = os.path.join(source_dir, "data")
# The locale data might be missing if "setup.py build" wasn't run.
cls.locale_dir = os.path.join(source_dir, "build", "mo")
# Not setting icon_dir. It is not useful since we don't employ the
# needed directory structure in the source dist.
def _init_excepthooks():
ExceptHookManager.setup()
ExceptHookManager.register_handler(ExitOnInterruptExceptionHandler())
def _init_paths(paths):
paths.ensure_setup()
def _init_locale(gettext_domain=None):
if Paths.locale_dir and gettext_domain is not None:
try:
locale.setlocale(locale.LC_ALL, "")
except locale.Error as exc:
from warnings import warn
warn("locale error: %s" % (exc,),
RuntimeWarning,
stacklevel=2)
Paths.locale_dir = None
else:
gettext.bindtextdomain(gettext_domain, Paths.locale_dir)
gettext.textdomain(gettext_domain)
gettext.bind_textdomain_codeset(gettext_domain, "UTF-8")
def _init_logging(level):
if level == "none":
return
mapping = {"debug": logging.DEBUG,
"info": logging.INFO,
"warning": logging.WARNING,
"error": logging.ERROR,
"critical": logging.CRITICAL}
logging.basicConfig(level=mapping[level],
format='%(asctime)s.%(msecs)03d %(levelname)8s %(name)20s: %(message)s',
datefmt='%H:%M:%S')
logger = logging.getLogger("main")
logger.debug("logging at level %s", logging.getLevelName(level))
logger.info("using Python %i.%i.%i %s %i", *sys.version_info)
def _init_log_option(parser):
choices = ["none", "debug", "info", "warning", "error", "critical"]
parser.add_option("--log-level", "-l",
type="choice",
choices=choices,
action="store",
dest="log_level",
default="none",
help=_("Enable logging, possible values: ") + ", ".join(choices))
return parser
def main(main_function, option_parser, gettext_domain=None, paths=None):
# FIXME:
global Paths
Paths = paths
_init_excepthooks()
_init_paths(paths)
_init_locale(gettext_domain)
parser = _init_log_option(option_parser)
options, args = option_parser.parse_args()
_init_logging(options.log_level)
try:
main_function(args)
finally:
logging.shutdown()
@@ -0,0 +1,25 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Development Utilities
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Development Utilities Common package."""
from . import Data
from . import GUI
from . import Main
from . import utils
@@ -0,0 +1,420 @@
# -*- Mode: Python; py-indent-offset: 4 -*-
# generictreemodel - GenericTreeModel implementation for pygtk compatibility.
# Copyright (C) 2013 Simon Feltman
#
# generictreemodel.py: GenericTreeModel implementation for pygtk compatibility
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see <http://www.gnu.org/licenses/>.
# System
import sys
import random
import collections
import ctypes
# GObject
from gi.repository import GObject
from gi.repository import Gtk
class _CTreeIter(ctypes.Structure):
_fields_ = [('stamp', ctypes.c_int),
('user_data', ctypes.c_void_p),
('user_data2', ctypes.c_void_p),
('user_data3', ctypes.c_void_p)]
@classmethod
def from_iter(cls, iter):
offset = sys.getsizeof(object()) # size of PyObject_HEAD
return ctypes.POINTER(cls).from_address(id(iter) + offset)
def _get_user_data_as_pyobject(iter):
citer = _CTreeIter.from_iter(iter)
return ctypes.cast(citer.contents.user_data, ctypes.py_object).value
def handle_exception(default_return):
"""Returns a function which can act as a decorator for wrapping exceptions and
returning "default_return" upon an exception being thrown.
This is used to wrap Gtk.TreeModel "do_" method implementations so we can return
a proper value from the override upon an exception occurring with client code
implemented by the "on_" methods.
"""
def decorator(func):
def wrapped_func(*args, **kargs):
try:
return func(*args, **kargs)
except BaseException:
# Use excepthook directly to avoid any printing to the screen
# if someone installed an except hook.
sys.excepthook(*sys.exc_info())
return default_return
return wrapped_func
return decorator
class GenericTreeModel(GObject.GObject, Gtk.TreeModel):
"""A base implementation of a Gtk.TreeModel for python.
The GenericTreeModel eases implementing the Gtk.TreeModel interface in Python.
The class can be subclassed to provide a TreeModel implementation which works
directly with Python objects instead of iterators.
All of the on_* methods should be overridden by subclasses to provide the
underlying implementation a way to access custom model data. For the purposes of
this API, all custom model data supplied or handed back through the overridable
API will use the argument names: node, parent, and child in regards to user data
python objects.
The create_tree_iter, set_user_data, invalidate_iters, iter_is_valid methods are
available to help manage Gtk.TreeIter objects and their Python object references.
GenericTreeModel manages a pool of user data nodes that have been used with iters.
This pool stores a references to user data nodes as a dictionary value with the
key being the integer id of the data. This id is what the Gtk.TreeIter objects
use to reference data in the pool.
References will be removed from the pool when the model is deleted or explicitly
by using the optional "node" argument to the "row_deleted" method when notifying
the model of row deletion.
"""
leak_references = GObject.Property(default=True, type=bool,
blurb="If True, strong references to user data attached to iters are "
"stored in a dictionary pool (default). Otherwise the user data is "
"stored as a raw pointer to a python object without a reference.")
#
# Methods
#
def __init__(self):
"""Initialize. Make sure to call this from derived classes if overridden."""
super(GenericTreeModel, self).__init__()
self.stamp = 0
#: Dictionary of (id(user_data): user_data), used when leak-refernces=False
self._held_refs = dict()
# Set initial stamp
self.invalidate_iters()
def iter_depth_first(self):
"""Depth-first iteration of the entire TreeModel yielding the python nodes."""
stack = collections.deque([None])
while stack:
it = stack.popleft()
if it is not None:
yield self.get_user_data(it)
children = [self.iter_nth_child(it, i)
for i in range(self.iter_n_children(it))]
stack.extendleft(reversed(children))
def invalidate_iter(self, iter):
"""Clear user data and its reference from the iter and this model."""
iter.stamp = 0
if iter.user_data:
if iter.user_data in self._held_refs:
del self._held_refs[iter.user_data]
iter.user_data = None
def invalidate_iters(self):
"""
This method invalidates all TreeIter objects associated with this custom tree model
and frees their locally pooled references.
"""
self.stamp = random.randint(-2147483648, 2147483647)
self._held_refs.clear()
def iter_is_valid(self, iter):
"""
:Returns:
True if the gtk.TreeIter specified by iter is valid for the custom tree model.
"""
return iter.stamp == self.stamp
def get_user_data(self, iter):
"""Get the user_data associated with the given TreeIter.
GenericTreeModel stores arbitrary Python objects mapped to instances of Gtk.TreeIter.
This method allows to retrieve the Python object held by the given iterator.
"""
if self.leak_references:
return self._held_refs[iter.user_data]
else:
return _get_user_data_as_pyobject(iter)
def set_user_data(self, iter, user_data):
"""Applies user_data and stamp to the given iter.
If the models "leak_references" property is set, a reference to the
user_data is stored with the model to ensure we don't run into bad
memory problems with the TreeIter.
"""
iter.user_data = id(user_data)
if user_data is None:
self.invalidate_iter(iter)
else:
iter.stamp = self.stamp
if self.leak_references:
self._held_refs[iter.user_data] = user_data
def create_tree_iter(self, user_data):
"""Create a Gtk.TreeIter instance with the given user_data specific for this model.
Use this method to create Gtk.TreeIter instance instead of directly calling
Gtk.Treeiter(), this will ensure proper reference managment of wrapped used_data.
"""
iter = Gtk.TreeIter()
self.set_user_data(iter, user_data)
return iter
def _create_tree_iter(self, data):
"""Internal creation of a (bool, TreeIter) pair for returning directly
back to the view interfacing with this model."""
if data is None:
return (False, None)
else:
it = self.create_tree_iter(data)
return (True, it)
def row_deleted(self, path, node=None):
"""Notify the model a row has been deleted.
Use the node parameter to ensure the user_data reference associated
with the path is properly freed by this model.
:Parameters:
path : Gtk.TreePath
Path to the row that has been deleted.
node : object
Python object used as the node returned from "on_get_iter". This is
optional but ensures the model will not leak references to this object.
"""
super(GenericTreeModel, self).row_deleted(path)
node_id = id(node)
if node_id in self._held_refs:
del self._held_refs[node_id]
#
# GtkTreeModel Interface Implementation
#
@handle_exception(0)
def do_get_flags(self):
"""Internal method."""
return self.on_get_flags()
@handle_exception(0)
def do_get_n_columns(self):
"""Internal method."""
return self.on_get_n_columns()
@handle_exception(GObject.TYPE_INVALID)
def do_get_column_type(self, index):
"""Internal method."""
return self.on_get_column_type(index)
@handle_exception((False, None))
def do_get_iter(self, path):
"""Internal method."""
return self._create_tree_iter(self.on_get_iter(path))
@handle_exception(False)
def do_iter_next(self, iter):
"""Internal method."""
if iter is None:
next_data = self.on_iter_next(None)
else:
next_data = self.on_iter_next(self.get_user_data(iter))
self.set_user_data(iter, next_data)
return next_data is not None
@handle_exception(None)
def do_get_path(self, iter):
"""Internal method."""
path = self.on_get_path(self.get_user_data(iter))
if path is None:
return None
else:
return Gtk.TreePath(path)
@handle_exception(None)
def do_get_value(self, iter, column):
"""Internal method."""
return self.on_get_value(self.get_user_data(iter), column)
@handle_exception((False, None))
def do_iter_children(self, parent):
"""Internal method."""
data = self.get_user_data(parent) if parent else None
return self._create_tree_iter(self.on_iter_children(data))
@handle_exception(False)
def do_iter_has_child(self, parent):
"""Internal method."""
return self.on_iter_has_child(self.get_user_data(parent))
@handle_exception(0)
def do_iter_n_children(self, iter):
"""Internal method."""
if iter is None:
return self.on_iter_n_children(None)
return self.on_iter_n_children(self.get_user_data(iter))
@handle_exception((False, None))
def do_iter_nth_child(self, parent, n):
"""Internal method."""
if parent is None:
data = self.on_iter_nth_child(None, n)
else:
data = self.on_iter_nth_child(self.get_user_data(parent), n)
return self._create_tree_iter(data)
@handle_exception((False, None))
def do_iter_parent(self, child):
"""Internal method."""
return self._create_tree_iter(self.on_iter_parent(self.get_user_data(child)))
@handle_exception(None)
def do_ref_node(self, iter):
self.on_ref_node(self.get_user_data(iter))
@handle_exception(None)
def do_unref_node(self, iter):
self.on_unref_node(self.get_user_data(iter))
#
# Python Subclass Overridables
#
def on_get_flags(self):
"""Overridable.
:Returns Gtk.TreeModelFlags:
The flags for this model. See: Gtk.TreeModelFlags
"""
raise NotImplementedError
def on_get_n_columns(self):
"""Overridable.
:Returns:
The number of columns for this model.
"""
raise NotImplementedError
def on_get_column_type(self, index):
"""Overridable.
:Returns:
The column type for the given index.
"""
raise NotImplementedError
def on_get_iter(self, path):
"""Overridable.
:Returns:
A python object (node) for the given TreePath.
"""
raise NotImplementedError
def on_iter_next(self, node):
"""Overridable.
:Parameters:
node : object
Node at current level.
:Returns:
A python object (node) following the given node at the current level.
"""
raise NotImplementedError
def on_get_path(self, node):
"""Overridable.
:Returns:
A TreePath for the given node.
"""
raise NotImplementedError
def on_get_value(self, node, column):
"""Overridable.
:Parameters:
node : object
column : int
Column index to get the value from.
:Returns:
The value of the column for the given node."""
raise NotImplementedError
def on_iter_children(self, parent):
"""Overridable.
:Returns:
The first child of parent or None if parent has no children.
If parent is None, return the first node of the model.
"""
raise NotImplementedError
def on_iter_has_child(self, node):
"""Overridable.
:Returns:
True if the given node has children.
"""
raise NotImplementedError
def on_iter_n_children(self, node):
"""Overridable.
:Returns:
The number of children for the given node. If node is None,
return the number of top level nodes.
"""
raise NotImplementedError
def on_iter_nth_child(self, parent, n):
"""Overridable.
:Parameters:
parent : object
n : int
Index of child within parent.
:Returns:
The child for the given parent index starting at 0. If parent None,
return the top level node corresponding to "n".
If "n" is larger then available nodes, return None.
"""
raise NotImplementedError
def on_iter_parent(self, child):
"""Overridable.
:Returns:
The parent node of child or None if child is a top level node."""
raise NotImplementedError
def on_ref_node(self, node):
pass
def on_unref_node(self, node):
pass
@@ -0,0 +1,333 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Development Utilities
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, see <http://www.gnu.org/licenses/>.
"""GStreamer Development Utilities Common utils module."""
import os
import logging
import subprocess as _subprocess
class SingletonMeta (type):
def __init__(cls, name, bases, dict_):
from weakref import WeakValueDictionary
super(SingletonMeta, cls).__init__(name, bases, dict_)
cls._singleton_instances = WeakValueDictionary()
def __call__(cls, *a, **kw):
kw_key = tuple(sorted(kw.items()))
try:
obj = cls._singleton_instances[a + kw_key]
except KeyError:
obj = super(SingletonMeta, cls).__call__(*a, **kw)
cls._singleton_instances[a + kw_key] = obj
return obj
def gettext_cache():
"""Return a callable object that operates like gettext.gettext, but is much
faster when a string is looked up more than once. This is very useful in
loops, where calling gettext.gettext can quickly become a major performance
bottleneck."""
from gettext import gettext
d = {}
def gettext_cache_access(s):
if s not in d:
d[s] = gettext(s)
return d[s]
return gettext_cache_access
class ClassProperty (property):
"Like the property class, but also invokes the getter for class access."
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
property.__init__(self, fget, fset, fdel, doc)
self.__fget = fget
def __get__(self, obj, obj_class=None):
ret = property.__get__(self, obj, obj_class)
if ret == self:
return self.__fget(None)
else:
return ret
class _XDGClass (object):
"""Partial implementation of the XDG Base Directory specification v0.6.
http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html"""
def __init__(self):
self._add_base_dir("DATA_HOME", "~/.local/share")
self._add_base_dir("CONFIG_HOME", "~/.config")
self._add_base_dir("CACHE_HOME", "~/.cache")
def _add_base_dir(self, name, default):
dir = os.environ.get("XDG_%s" % (name,))
if not dir:
dir = os.path.expanduser(os.path.join(*default.split("/")))
setattr(self, name, dir)
XDG = _XDGClass()
class SaveWriteFile (object):
def __init__(self, filename, mode="wt"):
from tempfile import mkstemp
self.logger = logging.getLogger("tempfile")
dir = os.path.dirname(filename)
base_name = os.path.basename(filename)
temp_prefix = "%s-tmp" % (base_name,)
if dir:
# Destination dir differs from current directory, ensure that it
# exists:
try:
os.makedirs(dir)
except OSError:
pass
self.clean_stale(dir, temp_prefix)
fd, temp_name = mkstemp(dir=dir, prefix=temp_prefix)
self.target_name = filename
self.temp_name = temp_name
self.real_file = os.fdopen(fd, mode)
def __enter__(self):
return self
def __exit__(self, *exc_args):
if exc_args == (None, None, None,):
self.close()
else:
self.discard()
def __del__(self):
try:
self.discard()
except AttributeError:
# If __init__ failed, self has no real_file attribute.
pass
def __close_real(self):
if self.real_file:
self.real_file.close()
self.real_file = None
def clean_stale(self, dir, temp_prefix):
from time import time
from glob import glob
now = time()
pattern = os.path.join(dir, "%s*" % (temp_prefix,))
for temp_filename in glob(pattern):
mtime = os.stat(temp_filename).st_mtime
if now - mtime > 3600:
self.logger.info("deleting stale temporary file %s",
temp_filename)
try:
os.unlink(temp_filename)
except EnvironmentError as exc:
self.logger.warning("deleting stale temporary file "
"failed: %s", exc)
def tell(self, *a, **kw):
return self.real_file.tell(*a, **kw)
def write(self, *a, **kw):
return self.real_file.write(*a, **kw)
def close(self):
self.__close_real()
if self.temp_name:
try:
os.rename(self.temp_name, self.target_name)
except OSError as exc:
import errno
if exc.errno == errno.EEXIST:
# We are probably on windows.
os.unlink(self.target_name)
os.rename(self.temp_name, self.target_name)
self.temp_name = None
def discard(self):
self.__close_real()
if self.temp_name:
try:
os.unlink(self.temp_name)
except EnvironmentError as exc:
self.logger.warning("deleting temporary file failed: %s", exc)
self.temp_name = None
class TeeWriteFile (object):
# TODO Py2.5: Add context manager methods.
def __init__(self, *file_objects):
self.files = list(file_objects)
def close(self):
for file in self.files:
file.close()
def flush(self):
for file in self.files:
file.flush()
def write(self, string):
for file in self.files:
file.write(string)
def writelines(self, lines):
for file in self.files:
file.writelines(lines)
class FixedPopen (_subprocess.Popen):
def __init__(self, args, **kw):
# Unconditionally specify all descriptors as redirected, to
# work around Python bug #1358527 (which is triggered for
# console-less applications on Windows).
close = []
for name in ("stdin", "stdout", "stderr",):
target = kw.get(name)
if not target:
kw[name] = _subprocess.PIPE
close.append(name)
_subprocess.Popen.__init__(self, args, **kw)
for name in close:
fp = getattr(self, name)
fp.close()
setattr(self, name, None)
class DevhelpError (EnvironmentError):
pass
class DevhelpUnavailableError (DevhelpError):
pass
class DevhelpClient (object):
def available(self):
try:
self.version()
except DevhelpUnavailableError:
return False
else:
return True
def version(self):
return self._invoke("--version")
def search(self, entry):
self._invoke_no_interact("-s", entry)
def _check_os_error(self, exc):
import errno
if exc.errno == errno.ENOENT:
raise DevhelpUnavailableError()
def _invoke(self, *args):
from subprocess import PIPE
try:
proc = FixedPopen(("devhelp",) + args,
stdout=PIPE)
except OSError as exc:
self._check_os_error(exc)
raise
out, err = proc.communicate()
if proc.returncode is not None and proc.returncode != 0:
raise DevhelpError("devhelp exited with status %i"
% (proc.returncode,))
return out
def _invoke_no_interact(self, *args):
from subprocess import PIPE
try:
proc = FixedPopen(("devhelp",) + args)
except OSError as exc:
self._check_os_error(exc)
raise
@@ -0,0 +1,482 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer Data module."""
import os
import logging
import re
import sys
# Nanosecond resolution (like Gst.SECOND)
SECOND = 1000000000
def time_args(ts):
secs = ts // SECOND
return "%i:%02i:%02i.%09i" % (secs // 60 ** 2,
secs // 60 % 60,
secs % 60,
ts % SECOND,)
def time_diff_args(time_diff):
if time_diff >= 0:
sign = "+"
else:
sign = "-"
secs = abs(time_diff) // SECOND
return "%s%02i:%02i.%09i" % (sign,
secs // 60,
secs % 60,
abs(time_diff) % SECOND,)
def time_args_no_hours(ts):
secs = ts // SECOND
return "%02i:%02i.%09i" % (secs // 60,
secs % 60,
ts % SECOND,)
def parse_time(st):
"""Parse time strings that look like "0:00:00.0000000"."""
h, m, s = st.split(":")
secs, subsecs = s.split(".")
return int((int(h) * 60 ** 2 + int(m) * 60) * SECOND) + \
int(secs) * SECOND + int(subsecs)
class DebugLevel (int):
__names = ["NONE", "ERROR", "WARN", "FIXME",
"INFO", "DEBUG", "LOG", "TRACE", "MEMDUMP"]
__instances = {}
def __new__(cls, level):
try:
level_int = int(level)
except (ValueError, TypeError,):
try:
level_int = cls.__names.index(level.upper())
except ValueError:
raise ValueError("no debug level named %r" % (level,))
if level_int in cls.__instances:
return cls.__instances[level_int]
else:
new_instance = int.__new__(cls, level_int)
new_instance.name = cls.__names[level_int]
cls.__instances[level_int] = new_instance
return new_instance
def __repr__(self):
return "<%s %s (%i)>" % (type(self).__name__, self.__names[self], self,)
def higher_level(self):
if self == len(self.__names) - 1:
raise ValueError("already the highest debug level")
return DebugLevel(self + 1)
def lower_level(self):
if self == 0:
raise ValueError("already the lowest debug level")
return DebugLevel(self - 1)
debug_level_none = DebugLevel("NONE")
debug_level_error = DebugLevel("ERROR")
debug_level_warning = DebugLevel("WARN")
debug_level_info = DebugLevel("INFO")
debug_level_debug = DebugLevel("DEBUG")
debug_level_log = DebugLevel("LOG")
debug_level_fixme = DebugLevel("FIXME")
debug_level_trace = DebugLevel("TRACE")
debug_level_memdump = DebugLevel("MEMDUMP")
debug_levels = [debug_level_none,
debug_level_trace,
debug_level_fixme,
debug_level_log,
debug_level_debug,
debug_level_info,
debug_level_warning,
debug_level_error,
debug_level_memdump]
# For stripping color codes:
_escape = re.compile(b"\x1b\\[[0-9;]*m")
def strip_escape(s):
# FIXME: This can be optimized further!
while b"\x1b" in s:
s = _escape.sub(b"", s)
return s
def default_log_line_regex_():
# "DEBUG "
LEVEL = "([A-Z]+)\s*"
# "0x8165430 "
THREAD = r"(0x[0-9a-f]+)\s+" # r"\((0x[0-9a-f]+) - "
# "0:00:00.777913000 "
TIME = r"(\d+:\d\d:\d\d\.\d+)\s+"
CATEGORY = "([A-Za-z0-9_-]+)\s+" # "GST_REFCOUNTING ", "flacdec "
# " 3089 "
PID = r"(\d+)\s*"
FILENAME = r"([^:]*):"
LINE = r"(\d+):"
FUNCTION = "(~?[A-Za-z0-9_\s\*,\(\)]*):"
# FIXME: When non-g(st)object stuff is logged with *_OBJECT (like
# buffers!), the address is printed *without* <> brackets!
OBJECT = "(?:<([^>]+)>)?"
MESSAGE = "(.+)"
ANSI = "(?:\x1b\\[[0-9;]*m\\s*)*\\s*"
# New log format:
expressions = [TIME, ANSI, PID, ANSI, THREAD, ANSI, LEVEL, ANSI,
CATEGORY, FILENAME, LINE, FUNCTION, ANSI,
OBJECT, ANSI, MESSAGE]
# Old log format:
# expressions = [LEVEL, THREAD, TIME, CATEGORY, PID, FILENAME, LINE,
# FUNCTION, OBJECT, MESSAGE]
return expressions
def default_log_line_regex():
return re.compile("".join(default_log_line_regex_()))
class Producer (object):
def __init__(self):
self.consumers = []
def have_load_started(self):
for consumer in self.consumers:
consumer.handle_load_started()
def have_load_finished(self):
for consumer in self.consumers:
consumer.handle_load_finished()
class SortHelper (object):
def __init__(self, fileobj, offsets):
self._gen = self.__gen(fileobj, offsets)
next(self._gen)
# Override in the instance, for performance (this gets called in an
# inner loop):
self.find_insert_position = self._gen.send
@staticmethod
def find_insert_position(insert_time_string):
# Stub for documentary purposes.
pass
@staticmethod
def __gen(fileobj, offsets):
from math import floor
tell = fileobj.tell
seek = fileobj.seek
read = fileobj.read
time_len = len(time_args(0))
# We remember the previous insertion point. This gives a nice speed up
# for larger bubbles which are already sorted. TODO: In practice, log
# lines only get out of order across threads. Need to check if it pays
# to parse the thread here and maintain multiple insertion points for
# heavily interleaved parts of the log.
pos = 0
pos_time_string = ""
insert_pos = None
while True:
insert_time_string = (yield insert_pos)
save_offset = tell()
if pos_time_string <= insert_time_string:
lo = pos
hi = len(offsets)
else:
lo = 0
hi = pos
# This is a bisection search, except we don't cut the range in the
# middle each time, but at the 90th percentile. This is because
# logs are "mostly sorted", so the insertion point is much more
# likely to be at the end anyways:
while lo < hi:
mid = int(floor(lo * 0.1 + hi * 0.9))
seek(offsets[mid])
mid_time_string = read(time_len)
if insert_time_string.encode('utf8') < mid_time_string:
hi = mid
else:
lo = mid + 1
pos = lo
# Caller will replace row at pos with the new one, so this is
# correct:
pos_time_string = insert_time_string
insert_pos = pos
seek(save_offset)
class LineCache (Producer):
"""
offsets: file position for each line
levels: the debug level for each line
"""
_lines_per_iteration = 50000
def __init__(self, fileobj, dispatcher):
Producer.__init__(self)
self.logger = logging.getLogger("linecache")
self.dispatcher = dispatcher
self.__fileobj = fileobj
self.__fileobj.seek(0, 2)
self.__file_size = self.__fileobj.tell()
self.__fileobj.seek(0)
self.offsets = []
self.levels = [] # FIXME
def start_loading(self):
self.logger.debug("dispatching load process")
self.have_load_started()
self.dispatcher(self.__process())
def get_progress(self):
return float(self.__fileobj.tell()) / self.__file_size
def __process(self):
offsets = self.offsets
levels = self.levels
dict_levels = {"T": debug_level_trace, "F": debug_level_fixme,
"L": debug_level_log, "D": debug_level_debug,
"I": debug_level_info, "W": debug_level_warning,
"E": debug_level_error, " ": debug_level_none,
"M": debug_level_memdump, }
ANSI = "(?:\x1b\\[[0-9;]*m)?"
ANSI_PATTERN = r"\d:\d\d:\d\d\.\d+ " + ANSI + \
r" *\d+" + ANSI + \
r" +0x[0-9a-f]+ +" + ANSI + \
r"([TFLDIEWM ])"
BARE_PATTERN = ANSI_PATTERN.replace(ANSI, "")
rexp_bare = re.compile(BARE_PATTERN)
rexp_ansi = re.compile(ANSI_PATTERN)
rexp = rexp_bare
# Moving attribute lookups out of the loop:
readline = self.__fileobj.readline
tell = self.__fileobj.tell
rexp_match = rexp.match
levels_append = levels.append
offsets_append = offsets.append
dict_levels_get = dict_levels.get
self.__fileobj.seek(0)
limit = self._lines_per_iteration
last_line = ""
i = 0
sort_helper = SortHelper(self.__fileobj, offsets)
find_insert_position = sort_helper.find_insert_position
while True:
i += 1
if i >= limit:
i = 0
yield True
offset = tell()
line = readline().decode('utf-8', errors='replace')
if not line:
break
match = rexp_match(line)
if match is None:
if rexp is rexp_ansi or "\x1b" not in line:
continue
match = rexp_ansi.match(line)
if match is None:
continue
# Switch to slower ANSI parsing:
rexp = rexp_ansi
rexp_match = rexp.match
# Timestamp is in the very beginning of the row, and can be sorted
# by lexical comparison. That's why we don't bother parsing the
# time to integer. We also don't have to take a substring here,
# which would be a useless memcpy.
if line >= last_line:
levels_append(
dict_levels_get(match.group(1), debug_level_none))
offsets_append(offset)
last_line = line
else:
pos = find_insert_position(line)
levels.insert(
pos, dict_levels_get(match.group(1), debug_level_none))
offsets.insert(pos, offset)
self.have_load_finished()
yield False
class LogLine (list):
_line_regex = default_log_line_regex()
@classmethod
def parse_full(cls, line_string):
match = cls._line_regex.match(line_string.decode('utf8', errors='replace'))
if match is None:
# raise ValueError ("not a valid log line (%r)" % (line_string,))
groups = [0, 0, 0, 0, "", "", 0, "", "", 0]
return cls(groups)
line = cls(match.groups())
# Timestamp.
line[0] = parse_time(line[0])
# PID.
line[1] = int(line[1])
# Thread.
line[2] = int(line[2], 16)
# Level (this is handled in LineCache).
line[3] = 0
# Line.
line[6] = int(line[6])
# Message start offset.
line[9] = match.start(9 + 1)
for col_id in (4, # COL_CATEGORY
5, # COL_FILENAME
7, # COL_FUNCTION,
8,): # COL_OBJECT
line[col_id] = sys.intern(line[col_id] or "")
return line
class LogLines (object):
def __init__(self, fileobj, line_cache):
self.__fileobj = fileobj
self.__line_cache = line_cache
def __len__(self):
return len(self.__line_cache.offsets)
def __getitem__(self, line_index):
offset = self.__line_cache.offsets[line_index]
self.__fileobj.seek(offset)
line_string = self.__fileobj.readline()
line = LogLine.parse_full(line_string)
msg = line_string[line[-1]:]
line[-1] = msg
return line
def __iter__(self):
size = len(self)
i = 0
while i < size:
yield self[i]
i += 1
class LogFile (Producer):
def __init__(self, filename, dispatcher):
import mmap
Producer.__init__(self)
self.logger = logging.getLogger("logfile")
self.path = os.path.normpath(os.path.abspath(filename))
self.__real_fileobj = open(filename, "rb")
self.fileobj = mmap.mmap(
self.__real_fileobj.fileno(), 0, access=mmap.ACCESS_READ)
self.line_cache = LineCache(self.fileobj, dispatcher)
self.line_cache.consumers.append(self)
def start_loading(self):
self.logger.debug("starting load")
self.line_cache.start_loading()
def get_load_progress(self):
return self.line_cache.get_progress()
def handle_load_started(self):
# Chain up to our consumers:
self.have_load_started()
def handle_load_finished(self):
self.logger.debug("finish loading")
self.lines = LogLines(self.fileobj, self.line_cache)
# Chain up to our consumers:
self.have_load_finished()
@@ -0,0 +1,44 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer GUI module."""
__author__ = u"René Stadler <mail@renestadler.de>"
__version__ = "0.1"
import gi
from GstDebugViewer.GUI.app import App
def main(args):
app = App()
# TODO: Once we support more than one window, open one window for each
# supplied filename.
window = app.windows[0]
if len(args) > 0:
window.set_log_file(args[0])
app.run()
if __name__ == "__main__":
main()
@@ -0,0 +1,158 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer GUI module."""
import os.path
import gi
gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '3.0')
from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gtk
from GstDebugViewer import Common
from GstDebugViewer.GUI.columns import ViewColumnManager
from GstDebugViewer.GUI.window import Window
class AppStateSection (Common.GUI.StateSection):
_name = "state"
geometry = Common.GUI.StateInt4("window-geometry")
maximized = Common.GUI.StateBool("window-maximized")
column_order = Common.GUI.StateItemList("column-order", ViewColumnManager)
columns_visible = Common.GUI.StateItemList(
"columns-visible", ViewColumnManager)
zoom_level = Common.GUI.StateInt("zoom-level")
class AppState (Common.GUI.State):
def __init__(self, *a, **kw):
Common.GUI.State.__init__(self, *a, **kw)
self.add_section_class(AppStateSection)
class App (object):
def __init__(self):
self.attach()
def load_plugins(self):
from GstDebugViewer import Plugins
plugin_classes = list(
Plugins.load([os.path.dirname(Plugins.__file__)]))
self.plugins = []
for plugin_class in plugin_classes:
plugin = plugin_class(self)
self.plugins.append(plugin)
def iter_plugin_features(self):
for plugin in self.plugins:
for feature in plugin.features:
yield feature
def attach(self):
config_home = Common.utils.XDG.CONFIG_HOME
state_filename = os.path.join(
config_home, "gst-debug-viewer", "state")
self.state = AppState(state_filename)
self.state_section = self.state.sections["state"]
self.load_plugins()
self.windows = []
# Apply custom widget stying
# TODO: check for dark theme
css = b"""
@define-color normal_bg_color #FFFFFF;
@define-color shade_bg_color shade(@normal_bg_color, 0.95);
#log_view row:nth-child(even) {
background-color: @normal_bg_color;
}
#log_view row:nth-child(odd) {
background-color: @shade_bg_color;
}
#log_view row:selected {
background-color: #4488FF;
}
#log_view {
-GtkTreeView-horizontal-separator: 0;
-GtkTreeView-vertical-separator: 1;
outline-width: 0;
outline-offset: 0;
}
"""
style_provider = Gtk.CssProvider()
style_provider.load_from_data(css)
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
self.open_window()
def detach(self):
# TODO: If we take over deferred saving from the inspector, specify now
# = True here!
self.state.save()
def run(self):
try:
Common.Main.MainLoopWrapper(Gtk.main, Gtk.main_quit).run()
except BaseException:
raise
else:
self.detach()
def open_window(self):
self.windows.append(Window(self))
def close_window(self, window):
self.windows.remove(window)
if not self.windows:
# GtkTreeView takes some time to go down for large files. Let's block
# until the window is hidden:
GObject.idle_add(Gtk.main_quit)
Gtk.main()
Gtk.main_quit()
@@ -0,0 +1,162 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer GUI module."""
from gi.repository import Gtk
from gi.repository import Gdk
from GstDebugViewer import Data
class Color (object):
def __init__(self, hex_24):
if hex_24.startswith("#"):
s = hex_24[1:]
else:
s = hex_24
self._fields = tuple((int(hs, 16) for hs in (s[:2], s[2:4], s[4:],)))
def gdk_color(self):
return Gdk.color_parse(self.hex_string())
def hex_string(self):
return "#%02x%02x%02x" % self._fields
def float_tuple(self):
return tuple((float(x) / 255 for x in self._fields))
def byte_tuple(self):
return self._fields
def short_tuple(self):
return tuple((x << 8 for x in self._fields))
class ColorPalette (object):
@classmethod
def get(cls):
try:
return cls._instance
except AttributeError:
cls._instance = cls()
return cls._instance
class TangoPalette (ColorPalette):
def __init__(self):
for name, r, g, b in [("black", 0, 0, 0,),
("white", 255, 255, 255,),
("butter1", 252, 233, 79),
("butter2", 237, 212, 0),
("butter3", 196, 160, 0),
("chameleon1", 138, 226, 52),
("chameleon2", 115, 210, 22),
("chameleon3", 78, 154, 6),
("orange1", 252, 175, 62),
("orange2", 245, 121, 0),
("orange3", 206, 92, 0),
("skyblue1", 114, 159, 207),
("skyblue2", 52, 101, 164),
("skyblue3", 32, 74, 135),
("plum1", 173, 127, 168),
("plum2", 117, 80, 123),
("plum3", 92, 53, 102),
("chocolate1", 233, 185, 110),
("chocolate2", 193, 125, 17),
("chocolate3", 143, 89, 2),
("scarletred1", 239, 41, 41),
("scarletred2", 204, 0, 0),
("scarletred3", 164, 0, 0),
("aluminium1", 238, 238, 236),
("aluminium2", 211, 215, 207),
("aluminium3", 186, 189, 182),
("aluminium4", 136, 138, 133),
("aluminium5", 85, 87, 83),
("aluminium6", 46, 52, 54)]:
setattr(self, name, Color("%02x%02x%02x" % (r, g, b,)))
class ColorTheme (object):
def __init__(self):
self.colors = {}
def add_color(self, key, *colors):
self.colors[key] = colors
class LevelColorTheme (ColorTheme):
pass
class LevelColorThemeTango (LevelColorTheme):
def __init__(self):
LevelColorTheme.__init__(self)
p = TangoPalette.get()
self.add_color(Data.debug_level_none, None, None, None)
self.add_color(Data.debug_level_trace, p.black, p.aluminium2)
self.add_color(Data.debug_level_fixme, p.black, p.butter3)
self.add_color(Data.debug_level_log, p.black, p.plum1)
self.add_color(Data.debug_level_debug, p.black, p.skyblue1)
self.add_color(Data.debug_level_info, p.black, p.chameleon1)
self.add_color(Data.debug_level_warning, p.black, p.orange1)
self.add_color(Data.debug_level_error, p.white, p.scarletred1)
self.add_color(Data.debug_level_memdump, p.white, p.aluminium3)
class ThreadColorTheme (ColorTheme):
pass
class ThreadColorThemeTango (ThreadColorTheme):
def __init__(self):
ThreadColorTheme.__init__(self)
t = TangoPalette.get()
for i, color in enumerate([t.butter2,
t.orange2,
t.chocolate3,
t.chameleon2,
t.skyblue1,
t.plum1,
t.scarletred1,
t.aluminium6]):
self.add_color(i, color)
@@ -0,0 +1,741 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer GUI module."""
import logging
from gi.repository import Gtk, GLib
from GstDebugViewer import Common, Data
from GstDebugViewer.GUI.colors import LevelColorThemeTango
from GstDebugViewer.GUI.models import LazyLogModel, LogModelBase
def _(s):
return s
# Sync with gst-inspector!
class Column (object):
"""A single list view column, managed by a ColumnManager instance."""
name = None
id = None
label_header = None
get_modify_func = None
get_data_func = None
get_sort_func = None
def __init__(self):
view_column = Gtk.TreeViewColumn(self.label_header)
view_column.props.reorderable = True
self.view_column = view_column
class SizedColumn (Column):
default_size = None
def compute_default_size(self):
return None
# Sync with gst-inspector?
class TextColumn (SizedColumn):
font_family = None
def __init__(self):
Column.__init__(self)
column = self.view_column
cell = Gtk.CellRendererText()
column.pack_start(cell, True)
cell.props.yalign = 0.
cell.props.ypad = 0
if self.font_family:
cell.props.family = self.font_family
cell.props.family_set = True
if self.get_data_func:
data_func = self.get_data_func()
assert data_func
id_ = self.id
if id_ is not None:
def cell_data_func(column, cell, model, tree_iter, user_data):
data_func(cell.props, model.get_value(tree_iter, id_))
else:
cell_data_func = data_func
column.set_cell_data_func(cell, cell_data_func)
elif not self.get_modify_func:
column.add_attribute(cell, "text", self.id)
else:
self.update_modify_func(column, cell)
column.props.resizable = True
def update_modify_func(self, column, cell):
modify_func = self.get_modify_func()
id_ = self.id
def cell_data_func(column, cell, model, tree_iter, user_data):
cell.props.text = modify_func(model.get_value(tree_iter, id_))
column.set_cell_data_func(cell, cell_data_func)
def compute_default_size(self):
values = self.get_values_for_size()
if not values:
return SizedColumn.compute_default_size(self)
cell = self.view_column.get_cells()[0]
if self.get_modify_func is not None:
format = self.get_modify_func()
else:
def identity(x):
return x
format = identity
max_width = 0
for value in values:
cell.props.text = format(value)
x, y, w, h = self.view_column.cell_get_size()
max_width = max(max_width, w)
return max_width
def get_values_for_size(self):
return ()
class TimeColumn (TextColumn):
name = "time"
label_header = _("Time")
id = LazyLogModel.COL_TIME
font_family = "monospace"
def __init__(self, *a, **kw):
self.base_time = 0
TextColumn.__init__(self, *a, **kw)
def get_modify_func(self):
if self.base_time:
time_diff_args = Data.time_diff_args
base_time = self.base_time
def format_time(value):
return time_diff_args(value - base_time)
else:
time_args = Data.time_args
def format_time(value):
# TODO: This is hard coded to omit hours.
return time_args(value)[2:]
return format_time
def get_values_for_size(self):
values = [0]
return values
def set_base_time(self, base_time):
self.base_time = base_time
column = self.view_column
cell = column.get_cells()[0]
self.update_modify_func(column, cell)
class LevelColumn (TextColumn):
name = "level"
label_header = _("L")
id = LazyLogModel.COL_LEVEL
def __init__(self):
TextColumn.__init__(self)
cell = self.view_column.get_cells()[0]
cell.props.xalign = .5
@staticmethod
def get_modify_func():
def format_level(value):
return value.name[0]
return format_level
@staticmethod
def get_data_func():
theme = LevelColorThemeTango()
colors = dict((level, tuple((c.gdk_color()
for c in theme.colors[level])),)
for level in Data.debug_levels
if level != Data.debug_level_none)
def level_data_func(cell_props, level):
cell_props.text = level.name[0]
if level in colors:
cell_colors = colors[level]
else:
cell_colors = (None, None, None,)
cell_props.foreground_gdk = cell_colors[0]
cell_props.background_gdk = cell_colors[1]
return level_data_func
def get_values_for_size(self):
values = [Data.debug_level_log, Data.debug_level_debug,
Data.debug_level_info, Data.debug_level_warning,
Data.debug_level_error, Data.debug_level_memdump]
return values
class PidColumn (TextColumn):
name = "pid"
label_header = _("PID")
id = LazyLogModel.COL_PID
font_family = "monospace"
@staticmethod
def get_modify_func():
return str
def get_values_for_size(self):
return ["999999"]
class ThreadColumn (TextColumn):
name = "thread"
label_header = _("Thread")
id = LazyLogModel.COL_THREAD
font_family = "monospace"
@staticmethod
def get_modify_func():
def format_thread(value):
return "0x%07x" % (value,)
return format_thread
def get_values_for_size(self):
return [int("ffffff", 16)]
class CategoryColumn (TextColumn):
name = "category"
label_header = _("Category")
id = LazyLogModel.COL_CATEGORY
def get_values_for_size(self):
return ["GST_LONG_CATEGORY", "somelongelement"]
class CodeColumn (TextColumn):
name = "code"
label_header = _("Code")
id = None
@staticmethod
def get_data_func():
filename_id = LogModelBase.COL_FILENAME
line_number_id = LogModelBase.COL_LINE_NUMBER
def filename_data_func(column, cell, model, tree_iter, user_data):
args = model.get(tree_iter, filename_id, line_number_id)
cell.props.text = "%s:%i" % args
return filename_data_func
def get_values_for_size(self):
return ["gstsomefilename.c:1234"]
class FunctionColumn (TextColumn):
name = "function"
label_header = _("Function")
id = LazyLogModel.COL_FUNCTION
def get_values_for_size(self):
return ["gst_this_should_be_enough"]
class ObjectColumn (TextColumn):
name = "object"
label_header = _("Object")
id = LazyLogModel.COL_OBJECT
def get_values_for_size(self):
return ["longobjectname00"]
class MessageColumn (TextColumn):
name = "message"
label_header = _("Message")
id = None
def __init__(self, *a, **kw):
self.highlighters = {}
TextColumn.__init__(self, *a, **kw)
def get_data_func(self):
highlighters = self.highlighters
id_ = LazyLogModel.COL_MESSAGE
def message_data_func(column, cell, model, tree_iter, user_data):
msg = model.get_value(tree_iter, id_).decode("utf8", errors="replace")
if not highlighters:
cell.props.text = msg
return
if len(highlighters) > 1:
raise NotImplementedError("FIXME: Support more than one...")
highlighter = list(highlighters.values())[0]
row = model[tree_iter]
ranges = highlighter(row)
if not ranges:
cell.props.text = msg
else:
tags = []
prev_end = 0
end = None
for start, end in ranges:
if prev_end < start:
tags.append(
GLib.markup_escape_text(msg[prev_end:start]))
msg_escape = GLib.markup_escape_text(msg[start:end])
tags.append("<span foreground=\'#FFFFFF\'"
" background=\'#0000FF\'>%s</span>" % (msg_escape,))
prev_end = end
if end is not None:
tags.append(GLib.markup_escape_text(msg[end:]))
cell.props.markup = "".join(tags)
return message_data_func
def get_values_for_size(self):
values = ["Just some good minimum size"]
return values
class ColumnManager (Common.GUI.Manager):
column_classes = ()
@classmethod
def iter_item_classes(cls):
return iter(cls.column_classes)
def __init__(self):
self.view = None
self.actions = None
self.zoom = 1.0
self.__columns_changed_id = None
self.columns = []
self.column_order = list(self.column_classes)
self.action_group = Gtk.ActionGroup("ColumnActions")
def make_entry(col_class):
return ("show-%s-column" % (col_class.name,),
None,
col_class.label_header,
None,
None,
None,
True,)
entries = [make_entry(cls) for cls in self.column_classes]
self.action_group.add_toggle_actions(entries)
def iter_items(self):
return iter(self.columns)
def attach(self):
for col_class in self.column_classes:
action = self.get_toggle_action(col_class)
if action.props.active:
self._add_column(col_class())
action.connect("toggled",
self.__handle_show_column_action_toggled,
col_class.name)
self.__columns_changed_id = self.view.connect("columns-changed",
self.__handle_view_columns_changed)
def detach(self):
if self.__columns_changed_id is not None:
self.view.disconnect(self.__columns_changed_id)
self.__columns_changed_id = None
def attach_sort(self):
sort_model = self.view.get_model()
# Inform the sorted tree model of any custom sorting functions.
for col_class in self.column_classes:
if col_class.get_sort_func:
sort_func = col_class.get_sort_func()
sort_model.set_sort_func(col_class.id, sort_func)
def enable_sort(self):
sort_model = self.view.get_model()
if sort_model:
self.logger.debug("activating sort")
sort_model.set_sort_column_id(*self.default_sort)
self.default_sort = None
else:
self.logger.debug("not activating sort (no model set)")
def disable_sort(self):
self.logger.debug("deactivating sort")
sort_model = self.view.get_model()
self.default_sort = tree_sortable_get_sort_column_id(sort_model)
sort_model.set_sort_column_id(TREE_SORTABLE_UNSORTED_COLUMN_ID,
Gtk.SortType.ASCENDING)
def set_zoom(self, scale):
for column in self.columns:
cell = column.view_column.get_cells()[0]
cell.props.scale = scale
column.view_column.queue_resize()
self.zoom = scale
def set_base_time(self, base_time):
try:
time_column = self.find_item(name=TimeColumn.name)
except KeyError:
return
time_column.set_base_time(base_time)
self.size_column(time_column)
def get_toggle_action(self, column_class):
action_name = "show-%s-column" % (column_class.name,)
return self.action_group.get_action(action_name)
def get_initial_column_order(self):
return tuple(self.column_classes)
def _add_column(self, column):
name = column.name
pos = self.__get_column_insert_position(column)
if self.view.props.fixed_height_mode:
column.view_column.props.sizing = Gtk.TreeViewColumnSizing.FIXED
cell = column.view_column.get_cells()[0]
cell.props.scale = self.zoom
self.columns.insert(pos, column)
self.view.insert_column(column.view_column, pos)
def _remove_column(self, column):
self.columns.remove(column)
self.view.remove_column(column.view_column)
def __get_column_insert_position(self, column):
col_class = self.find_item_class(name=column.name)
pos = self.column_order.index(col_class)
before = self.column_order[:pos]
shown_names = [col.name for col in self.columns]
for col_class in before:
if col_class.name not in shown_names:
pos -= 1
return pos
def __iter_next_hidden(self, column_class):
pos = self.column_order.index(column_class)
rest = self.column_order[pos + 1:]
for next_class in rest:
try:
self.find_item(name=next_class.name)
except KeyError:
# No instance -- the column is hidden.
yield next_class
else:
break
def __handle_show_column_action_toggled(self, toggle_action, name):
if toggle_action.props.active:
try:
# This should fail.
column = self.find_item(name=name)
except KeyError:
col_class = self.find_item_class(name=name)
self._add_column(col_class())
else:
# Out of sync for some reason.
return
else:
try:
column = self.find_item(name=name)
except KeyError:
# Out of sync for some reason.
return
else:
self._remove_column(column)
def __handle_view_columns_changed(self, element_view):
view_columns = element_view.get_columns()
new_visible = [self.find_item(view_column=column)
for column in view_columns]
# We only care about reordering here.
if len(new_visible) != len(self.columns):
return
if new_visible != self.columns:
new_order = []
for column in new_visible:
col_class = self.find_item_class(name=column.name)
new_order.append(col_class)
new_order.extend(self.__iter_next_hidden(col_class))
names = (column.name for column in new_visible)
self.logger.debug("visible columns reordered: %s",
", ".join(names))
self.columns[:] = new_visible
self.column_order[:] = new_order
class ViewColumnManager (ColumnManager):
column_classes = (
TimeColumn, LevelColumn, PidColumn, ThreadColumn, CategoryColumn,
CodeColumn, FunctionColumn, ObjectColumn, MessageColumn,)
default_column_classes = (
TimeColumn, LevelColumn, CategoryColumn, CodeColumn,
FunctionColumn, ObjectColumn, MessageColumn,)
def __init__(self, state):
ColumnManager.__init__(self)
self.logger = logging.getLogger("ui.columns")
self.state = state
def attach(self, view):
self.view = view
view.connect("notify::model", self.__handle_notify_model)
order = self.state.column_order
if len(order) == len(self.column_classes):
self.column_order[:] = order
visible = self.state.columns_visible
if not visible:
visible = self.default_column_classes
for col_class in self.column_classes:
action = self.get_toggle_action(col_class)
action.props.active = (col_class in visible)
ColumnManager.attach(self)
self.columns_sized = False
def detach(self):
self.state.column_order = self.column_order
self.state.columns_visible = self.columns
return ColumnManager.detach(self)
def set_zoom(self, scale):
ColumnManager.set_zoom(self, scale)
if self.view is None:
return
# Timestamp and log level columns are pretty much fixed size, so resize
# them back to default on zoom change:
names = (TimeColumn.name,
LevelColumn.name,
PidColumn.name,
ThreadColumn.name)
for column in self.columns:
if column.name in names:
self.size_column(column)
def size_column(self, column):
if column.default_size is None:
default_size = column.compute_default_size()
else:
default_size = column.default_size
# FIXME: Abstract away fixed size setting in Column class!
if default_size is None:
# Dummy fallback:
column.view_column.props.fixed_width = 50
self.logger.warning(
"%s column does not implement default size", column.name)
else:
column.view_column.props.fixed_width = default_size
def _add_column(self, column):
result = ColumnManager._add_column(self, column)
self.size_column(column)
return result
def _remove_column(self, column):
column.default_size = column.view_column.props.fixed_width
return ColumnManager._remove_column(self, column)
def __handle_notify_model(self, view, gparam):
if self.columns_sized:
# Already sized.
return
model = self.view.get_model()
if model is None:
return
self.logger.debug("model changed, sizing columns")
for column in self.iter_items():
self.size_column(column)
self.columns_sized = True
class WrappingMessageColumn (MessageColumn):
def wrap_to_width(self, width):
col = self.view_column
col.props.max_width = width
col.get_cells()[0].props.wrap_width = width
col.queue_resize()
class LineViewColumnManager (ColumnManager):
column_classes = (TimeColumn, WrappingMessageColumn,)
def __init__(self):
ColumnManager.__init__(self)
def attach(self, window):
self.__size_update = None
self.view = window.widgets.line_view
self.view.set_size_request(0, 0)
self.view.connect_after("size-allocate", self.__handle_size_allocate)
ColumnManager.attach(self)
def __update_sizes(self):
view_width = self.view.get_allocation().width
if view_width == self.__size_update:
# Prevent endless recursion.
return
self.__size_update = view_width
col = self.find_item(name="time")
other_width = col.view_column.props.width
try:
col = self.find_item(name="message")
except KeyError:
return
width = view_width - other_width
col.wrap_to_width(width)
def __handle_size_allocate(self, self_, allocation):
self.__update_sizes()
@@ -0,0 +1,114 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer GUI module."""
from GstDebugViewer.GUI.models import LogModelBase
def get_comparison_function(all_but_this):
if (all_but_this):
return lambda x, y: x == y
else:
return lambda x, y: x != y
class Filter (object):
pass
class DebugLevelFilter (Filter):
only_this, all_but_this, this_and_above = range(3)
def __init__(self, debug_level, mode=0):
col_id = LogModelBase.COL_LEVEL
if mode == self.this_and_above:
def comparison_function(x, y):
return x < y
else:
comparison_function = get_comparison_function(
mode == self.all_but_this)
def filter_func(row):
return comparison_function(row[col_id], debug_level)
self.filter_func = filter_func
class CategoryFilter (Filter):
def __init__(self, category, all_but_this=False):
col_id = LogModelBase.COL_CATEGORY
comparison_function = get_comparison_function(all_but_this)
def category_filter_func(row):
return comparison_function(row[col_id], category)
self.filter_func = category_filter_func
class ObjectFilter (Filter):
def __init__(self, object_, all_but_this=False):
col_id = LogModelBase.COL_OBJECT
comparison_function = get_comparison_function(all_but_this)
def object_filter_func(row):
return comparison_function(row[col_id], object_)
self.filter_func = object_filter_func
class FunctionFilter (Filter):
def __init__(self, function_, all_but_this=False):
col_id = LogModelBase.COL_FUNCTION
comparison_function = get_comparison_function(all_but_this)
def function_filter_func(row):
return comparison_function(row[col_id], function_)
self.filter_func = function_filter_func
class ThreadFilter (Filter):
def __init__(self, thread_, all_but_this=False):
col_id = LogModelBase.COL_THREAD
comparison_function = get_comparison_function(all_but_this)
def thread_filter_func(row):
return comparison_function(row[col_id], thread_)
self.filter_func = thread_filter_func
class FilenameFilter (Filter):
def __init__(self, filename, all_but_this=False):
col_id = LogModelBase.COL_FILENAME
comparison_function = get_comparison_function(all_but_this)
def filename_filter_func(row):
return comparison_function(row[col_id], filename)
self.filter_func = filename_filter_func
@@ -0,0 +1,498 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer GUI module."""
from array import array
from bisect import bisect_left
import logging
from gi.repository import GObject
from gi.repository import Gtk
from GstDebugViewer import Common, Data
class LogModelBase (Common.GUI.GenericTreeModel, metaclass=Common.GUI.MetaModel):
columns = ("COL_TIME", GObject.TYPE_UINT64,
"COL_PID", int,
"COL_THREAD", GObject.TYPE_UINT64,
"COL_LEVEL", object,
"COL_CATEGORY", str,
"COL_FILENAME", str,
"COL_LINE_NUMBER", int,
"COL_FUNCTION", str,
"COL_OBJECT", str,
"COL_MESSAGE", str,)
def __init__(self):
Common.GUI.GenericTreeModel.__init__(self)
# self.props.leak_references = False
self.line_offsets = array("I")
self.line_levels = [] # FIXME: Not so nice!
self.line_cache = {}
def ensure_cached(self, line_offset):
raise NotImplementedError("derived classes must override this method")
def access_offset(self, offset):
raise NotImplementedError("derived classes must override this method")
def iter_rows_offset(self):
ensure_cached = self.ensure_cached
line_cache = self.line_cache
line_levels = self.line_levels
COL_LEVEL = self.COL_LEVEL
COL_MESSAGE = self.COL_MESSAGE
access_offset = self.access_offset
for i, offset in enumerate(self.line_offsets):
ensure_cached(offset)
row = line_cache[offset]
# adjust special rows
row[COL_LEVEL] = line_levels[i]
msg_offset = row[COL_MESSAGE]
row[COL_MESSAGE] = access_offset(offset + msg_offset)
yield (row, offset,)
row[COL_MESSAGE] = msg_offset
def on_get_flags(self):
flags = Gtk.TreeModelFlags.LIST_ONLY | Gtk.TreeModelFlags.ITERS_PERSIST
return flags
def on_get_n_columns(self):
return len(self.column_types)
def on_get_column_type(self, col_id):
return self.column_types[col_id]
def on_get_iter(self, path):
if not path:
return
if len(path) > 1:
# Flat model.
return None
line_index = path[0]
if line_index > len(self.line_offsets) - 1:
return None
return line_index
def on_get_path(self, rowref):
line_index = rowref
return (line_index,)
def on_get_value(self, line_index, col_id):
last_index = len(self.line_offsets) - 1
if line_index > last_index:
return None
if col_id == self.COL_LEVEL:
return self.line_levels[line_index]
line_offset = self.line_offsets[line_index]
self.ensure_cached(line_offset)
value = self.line_cache[line_offset][col_id]
if col_id == self.COL_MESSAGE:
# strip whitespace + newline
value = self.access_offset(line_offset + value).strip()
elif col_id in (self.COL_TIME, self.COL_THREAD):
value = GObject.Value(GObject.TYPE_UINT64, value)
return value
def get_value_range(self, col_id, start, stop):
if col_id != self.COL_LEVEL:
raise NotImplementedError("XXX FIXME")
return self.line_levels[start:stop]
def on_iter_next(self, line_index):
last_index = len(self.line_offsets) - 1
if line_index >= last_index:
return None
else:
return line_index + 1
def on_iter_children(self, parent):
return self.on_iter_nth_child(parent, 0)
def on_iter_has_child(self, rowref):
return False
def on_iter_n_children(self, rowref):
if rowref is not None:
return 0
return len(self.line_offsets)
def on_iter_nth_child(self, parent, n):
last_index = len(self.line_offsets) - 1
if parent or n > last_index:
return None
return n
def on_iter_parent(self, child):
return None
# def on_ref_node (self, rowref):
# pass
# def on_unref_node (self, rowref):
# pass
class LazyLogModel (LogModelBase):
def __init__(self, log_obj=None):
LogModelBase.__init__(self)
self.__log_obj = log_obj
if log_obj:
self.set_log(log_obj)
def set_log(self, log_obj):
self.__fileobj = log_obj.fileobj
self.line_cache.clear()
self.line_offsets = log_obj.line_cache.offsets
self.line_levels = log_obj.line_cache.levels
def access_offset(self, offset):
# TODO: Implement using one slice access instead of seek+readline.
self.__fileobj.seek(offset)
return self.__fileobj.readline()
def ensure_cached(self, line_offset):
if line_offset in self.line_cache:
return
if len(self.line_cache) > 10000:
self.line_cache.clear()
self.__fileobj.seek(line_offset)
line = self.__fileobj.readline()
self.line_cache[line_offset] = Data.LogLine.parse_full(line)
class FilteredLogModelBase (LogModelBase):
def __init__(self, super_model):
LogModelBase.__init__(self)
self.logger = logging.getLogger("filter-model-base")
self.super_model = super_model
self.access_offset = super_model.access_offset
self.ensure_cached = super_model.ensure_cached
self.line_cache = super_model.line_cache
def line_index_to_super(self, line_index):
raise NotImplementedError("index conversion not supported")
def line_index_from_super(self, super_line_index):
raise NotImplementedError("index conversion not supported")
class FilteredLogModel (FilteredLogModelBase):
def __init__(self, super_model):
FilteredLogModelBase.__init__(self, super_model)
self.logger = logging.getLogger("filtered-log-model")
self.filters = []
self.reset()
self.__active_process = None
self.__filter_progress = 0.
def reset(self):
self.logger.debug("reset filter")
self.line_offsets = self.super_model.line_offsets
self.line_levels = self.super_model.line_levels
self.super_index = range(len(self.line_offsets))
del self.filters[:]
def __filter_process(self, filter):
YIELD_LIMIT = 10000
self.logger.debug("preparing new filter")
new_line_offsets = array("I")
new_line_levels = []
new_super_index = array("I")
level_id = self.COL_LEVEL
func = filter.filter_func
def enum():
i = 0
for row, offset in self.iter_rows_offset():
line_index = self.super_index[i]
yield (line_index, row, offset,)
i += 1
self.logger.debug("running filter")
progress = 0.
progress_full = float(len(self))
y = YIELD_LIMIT
for i, row, offset in enum():
if func(row):
new_line_offsets.append(offset)
new_line_levels.append(row[level_id])
new_super_index.append(i)
y -= 1
if y == 0:
progress += float(YIELD_LIMIT)
self.__filter_progress = progress / progress_full
y = YIELD_LIMIT
yield True
self.line_offsets = new_line_offsets
self.line_levels = new_line_levels
self.super_index = new_super_index
self.logger.debug("filtering finished")
self.__filter_progress = 1.
self.__handle_filter_process_finished()
yield False
def add_filter(self, filter, dispatcher):
if self.__active_process is not None:
raise ValueError("dispatched a filter process already")
self.logger.debug("adding filter")
self.filters.append(filter)
self.__dispatcher = dispatcher
self.__active_process = self.__filter_process(filter)
dispatcher(self.__active_process)
def abort_process(self):
if self.__active_process is None:
raise ValueError("no filter process running")
self.__dispatcher.cancel()
self.__active_process = None
self.__dispatcher = None
del self.filters[-1]
def get_filter_progress(self):
if self.__active_process is None:
raise ValueError("no filter process running")
return self.__filter_progress
def __handle_filter_process_finished(self):
self.__active_process = None
self.handle_process_finished()
def handle_process_finished(self):
pass
def line_index_from_super(self, super_line_index):
return bisect_left(self.super_index, super_line_index)
def line_index_to_super(self, line_index):
return self.super_index[line_index]
def set_range(self, super_start, super_stop):
old_super_start = self.line_index_to_super(0)
old_super_stop = self.line_index_to_super(
len(self.super_index) - 1) + 1
self.logger.debug("set range (%i, %i), current (%i, %i)",
super_start, super_stop, old_super_start, old_super_stop)
if len(self.filters) == 0:
# Identity.
self.super_index = range(super_start, super_stop)
self.line_offsets = SubRange(self.super_model.line_offsets,
super_start, super_stop)
self.line_levels = SubRange(self.super_model.line_levels,
super_start, super_stop)
return
if super_start < old_super_start:
# TODO:
raise NotImplementedError("Only handling further restriction of the range"
" (start offset = %i)" % (super_start,))
if super_stop > old_super_stop:
# TODO:
raise NotImplementedError("Only handling further restriction of the range"
" (end offset = %i)" % (super_stop,))
start = self.line_index_from_super(super_start)
stop = self.line_index_from_super(super_stop)
self.super_index = SubRange(self.super_index, start, stop)
self.line_offsets = SubRange(self.line_offsets, start, stop)
self.line_levels = SubRange(self.line_levels, start, stop)
class SubRange (object):
__slots__ = ("size", "start", "stop",)
def __init__(self, size, start, stop):
if start > stop:
raise ValueError(
"need start <= stop (got %r, %r)" % (start, stop,))
if isinstance(size, type(self)):
# Another SubRange, don't stack:
start += size.start
stop += size.start
size = size.size
self.size = size
self.start = start
self.stop = stop
def __getitem__(self, i):
if isinstance(i, slice):
stop = i.stop
if stop >= 0:
stop += self.start
else:
stop += self.stop
return self.size[i.start + self.start:stop]
else:
return self.size[i + self.start]
def __len__(self):
return self.stop - self.start
def __iter__(self):
size = self.size
for i in range(self.start, self.stop):
yield size[i]
class LineViewLogModel (FilteredLogModelBase):
def __init__(self, super_model):
FilteredLogModelBase.__init__(self, super_model)
self.line_offsets = []
self.line_levels = []
self.parent_indices = []
def reset(self):
del self.line_offsets[:]
del self.line_levels[:]
def line_index_to_super(self, line_index):
return self.parent_indices[line_index]
def insert_line(self, position, super_line_index):
if position == -1:
position = len(self.line_offsets)
li = super_line_index
self.line_offsets.insert(position, self.super_model.line_offsets[li])
self.line_levels.insert(position, self.super_model.line_levels[li])
self.parent_indices.insert(position, super_line_index)
path = (position,)
tree_iter = self.get_iter(path)
self.row_inserted(path, tree_iter)
def replace_line(self, line_index, super_line_index):
li = line_index
self.line_offsets[li] = self.super_model.line_offsets[super_line_index]
self.line_levels[li] = self.super_model.line_levels[super_line_index]
self.parent_indices[li] = super_line_index
path = (line_index,)
tree_iter = self.get_iter(path)
self.row_changed(path, tree_iter)
def remove_line(self, line_index):
for l in (self.line_offsets,
self.line_levels,
self.parent_indices,):
del l[line_index]
path = (line_index,)
self.row_deleted(path)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer Main module."""
import sys
import optparse
from gettext import gettext as _, ngettext
from gi.repository import GLib
from GstDebugViewer import GUI
import GstDebugViewer.Common.Main
Common = GstDebugViewer.Common
GETTEXT_DOMAIN = "gst-debug-viewer"
def main_version(opt, value, parser, *args, **kwargs):
from GstDebugViewer import version
print("GStreamer Debug Viewer %s" % (version,))
sys.exit(0)
class Paths (Common.Main.PathsProgramBase):
program_name = "gst-debug-viewer"
def main():
parser = optparse.OptionParser(
_("%prog [OPTION...] [FILENAME]"),
description=_("Display and analyze GStreamer debug log files"))
parser.add_option("--version", "-v",
action="callback",
dest="version",
callback=main_version,
help=_("Display version and exit"))
Common.Main.main(main_function=GUI.main,
option_parser=parser,
gettext_domain=GETTEXT_DOMAIN,
paths=Paths)
@@ -0,0 +1,497 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer timeline widget plugin."""
import logging
from GstDebugViewer import Common, Data, GUI
from GstDebugViewer.Plugins import FeatureBase, PluginBase, _N
from gettext import gettext as _
from gi.repository import GObject, GLib
from gi.repository import Gtk
class SearchOperation (object):
def __init__(self, model, search_text, search_forward=True, start_position=None):
self.model = model
if isinstance(search_text, str):
self.search_text = search_text.encode('utf8')
else:
self.search_text = search_text
self.search_forward = search_forward
self.start_position = start_position
col_id = GUI.models.LogModelBase.COL_MESSAGE
len_search_text = len(self.search_text)
def match_func(model_row):
message = model_row[col_id]
if self.search_text in message:
ranges = []
start = 0
while True:
pos = message.find(self.search_text, start)
if pos == -1:
break
ranges.append((pos, pos + len_search_text,))
start = pos + len_search_text
return ranges
else:
return ()
self.match_func = match_func
class SearchSentinel (object):
def __init__(self):
self.dispatcher = Common.Data.GSourceDispatcher()
self.cancelled = False
def run_for(self, operation):
self.dispatcher.cancel()
self.dispatcher(self.__process(operation))
self.cancelled = False
def abort(self):
self.dispatcher.cancel()
self.cancelled = True
def __process(self, operation):
model = operation.model
if operation.start_position is not None:
start_pos = operation.start_position
elif operation.search_forward:
start_pos = 0
else:
start_pos = len(model) - 1
start_iter = model.iter_nth_child(None, start_pos)
match_func = operation.match_func
if operation.search_forward:
iter_next = model.iter_next
else:
# FIXME: This is really ugly.
nth_child = model.iter_nth_child
def iter_next_():
for i in range(start_pos, -1, -1):
yield nth_child(None, i)
yield None
it_ = iter_next_()
def iter_next(it):
return it_.__next__()
YIELD_LIMIT = 1000
i = YIELD_LIMIT
tree_iter = start_iter
while tree_iter and not self.cancelled:
i -= 1
if i == 0:
yield True
i = YIELD_LIMIT
row = model[tree_iter]
if match_func(row):
self.handle_match_found(model, tree_iter)
tree_iter = iter_next(tree_iter)
if not self.cancelled:
self.handle_search_complete()
yield False
def handle_match_found(self, model, tree_iter):
pass
def handle_search_complete(self):
pass
class FindBarWidget (Gtk.HBox):
__status = {"no-match-found": _N("No match found"),
"searching": _N("Searching...")}
def __init__(self, action_group):
GObject.GObject.__init__(self)
label = Gtk.Label(label=_("Find:"))
self.pack_start(label, False, False, 2)
self.entry = Gtk.Entry()
self.pack_start(self.entry, True, True, 0)
prev_action = action_group.get_action("goto-previous-search-result")
prev_button = Gtk.Button()
prev_button.set_related_action(prev_action)
self.pack_start(prev_button, False, False, 0)
next_action = action_group.get_action("goto-next-search-result")
next_button = Gtk.Button()
next_button.set_related_action(next_action)
self.pack_start(next_button, False, False, 0)
self.status_label = Gtk.Label()
self.status_label.props.xalign = 0.
self.status_label.props.use_markup = True
self.pack_start(self.status_label, False, False, 6)
self.__compute_status_size()
self.status_label.connect("notify::style", self.__handle_notify_style)
self.show_all()
def __compute_status_size(self):
label = self.status_label
old_markup = label.props.label
label.set_size_request(-1, -1)
max_width = 0
try:
for status in self.__status.values():
self.__set_status(_(status))
req = label.size_request()
max_width = max(max_width, req.width)
label.set_size_request(max_width, -1)
finally:
label.props.label = old_markup
def __handle_notify_style(self, *a, **kw):
self.__compute_status_size()
def __set_status(self, text):
markup = "<b>%s</b>" % (GLib.markup_escape_text(text),)
self.status_label.props.label = markup
def status_no_match_found(self):
self.__set_status(_(self.__status["no-match-found"]))
def status_searching(self):
self.__set_status(_(self.__status["searching"]))
def clear_status(self):
self.__set_status("")
class FindBarFeature (FeatureBase):
def __init__(self, app):
FeatureBase.__init__(self, app)
self.logger = logging.getLogger("ui.findbar")
self.action_group = Gtk.ActionGroup("FindBarActions")
self.action_group.add_toggle_actions([("show-find-bar",
None,
_("Find Bar"),
"<Ctrl>F")])
self.action_group.add_actions([("goto-next-search-result",
None, _("Goto Next Match"),
"<Ctrl>G"),
("goto-previous-search-result",
None, _("Goto Previous Match"),
"<Ctrl><Shift>G")])
self.bar = None
self.operation = None
self.search_state = None
self.next_match = None
self.prev_match = None
self.scroll_match = False
self.sentinel = SearchSentinel()
self.sentinel.handle_match_found = self.handle_match_found
self.sentinel.handle_search_complete = self.handle_search_complete
def scroll_view_to_line(self, line_index):
view = self.log_view
path = Gtk.TreePath((line_index,))
start_path, end_path = view.get_visible_range()
if path >= start_path and path <= end_path:
self.logger.debug(
"line index %i already visible, not scrolling", line_index)
return
self.logger.debug("scrolling to line_index %i", line_index)
view.scroll_to_cell(path, use_align=True, row_align=.5)
def handle_attach_window(self, window):
self.window = window
ui = window.ui_manager
ui.insert_action_group(self.action_group, 0)
self.log_view = window.log_view
self.merge_id = ui.new_merge_id()
for name, action_name in [("ViewFindBar", "show-find-bar",),
("ViewNextResult",
"goto-next-search-result",),
("ViewPrevResult", "goto-previous-search-result",)]:
ui.add_ui(self.merge_id, "/menubar/ViewMenu/ViewMenuAdditions",
name, action_name, Gtk.UIManagerItemType.MENUITEM, False)
box = window.widgets.vbox_view
self.bar = FindBarWidget(self.action_group)
box.pack_end(self.bar, False, False, 0)
self.bar.hide()
action = self.action_group.get_action("show-find-bar")
handler = self.handle_show_find_bar_action_toggled
action.connect("toggled", handler)
action = self.action_group.get_action("goto-previous-search-result")
handler = self.handle_goto_previous_search_result_action_activate
action.props.sensitive = False
action.connect("activate", handler)
action = self.action_group.get_action("goto-next-search-result")
handler = self.handle_goto_next_search_result_action_activate
action.props.sensitive = False
action.connect("activate", handler)
self.bar.entry.connect("changed", self.handle_entry_changed)
def handle_detach_window(self, window):
self.window = None
window.ui_manager.remove_ui(self.merge_id)
self.merge_id = None
def handle_show_find_bar_action_toggled(self, action):
if action.props.active:
self.bar.show()
self.bar.entry.grab_focus()
self.update_search()
else:
try:
column = self.window.column_manager.find_item(
name="message")
del column.highlighters[self]
except KeyError:
pass
self.bar.clear_status()
self.bar.hide()
for action_name in ["goto-next-search-result",
"goto-previous-search-result"]:
self.action_group.get_action(
action_name).props.sensitive = False
def handle_goto_previous_search_result_action_activate(self, action):
if self.prev_match is None:
self.logger.warning("inconsistent action sensitivity")
return
self.scroll_view_to_line(self.prev_match)
self.prev_match = None
start_path = self.log_view.get_visible_range()[0]
new_position = start_path[0] - 1
self.start_search_operation(start_position=new_position,
forward=False)
# FIXME
def handle_goto_next_search_result_action_activate(self, action):
if self.next_match is None:
self.logger.warning("inconsistent action sensitivity")
return
self.scroll_view_to_line(self.next_match)
self.next_match = None
end_path = self.log_view.get_visible_range()[1]
new_position = end_path[0] + 1
self.start_search_operation(start_position=new_position,
forward=True)
# FIXME: Finish.
# model = self.log_view.get_model ()
# start_path, end_path = self.log_view.get_visible_range ()
# start_index, end_index = start_path[0], end_path[0]
# for line_index in self.matches:
# if line_index > end_index:
# break
# else:
# return
# self.scroll_view_to_line (line_index)
def handle_entry_changed(self, entry):
self.update_search()
def update_search(self):
model = self.log_view.get_model()
search_text = self.bar.entry.props.text
column = self.window.column_manager.find_item(name="message")
if search_text == "":
self.logger.debug("search string set to '', aborting search")
self.search_state = None
self.next_match = None
self.prev_match = None
self.update_sensitivity()
self.sentinel.abort()
try:
del column.highlighters[self]
except KeyError:
pass
else:
self.logger.debug("starting search for %r", search_text)
self.next_match = None
self.prev_match = None
self.update_sensitivity()
self.scroll_match = True
start_path = self.log_view.get_visible_range()[0]
self.start_search_operation(
search_text, start_position=start_path[0])
self.bar.status_searching()
column.highlighters[self] = self.operation.match_func
self.window.update_view()
def update_sensitivity(self):
for name, value in (("goto-next-search-result", self.next_match,),
("goto-previous-search-result", self.prev_match,),):
action = self.action_group.get_action(name)
action.props.sensitive = (value is not None)
def start_search_operation(self, search_text=None, forward=True, start_position=None):
model = self.log_view.get_model()
if forward:
self.search_state = "search-forward"
if start_position is None:
start_position = 0
else:
self.search_state = "search-backward"
if start_position is None:
start_position = len(model) - 1
if search_text is None:
operation = self.operation
if operation is None:
raise ValueError(
"search_text not given but have no previous search operation")
search_text = operation.search_text
self.operation = SearchOperation(model, search_text,
start_position=start_position,
search_forward=forward)
self.sentinel.run_for(self.operation)
def handle_match_found(self, model, tree_iter):
if self.search_state not in ("search-forward", "search-backward",):
self.logger.warning(
"inconsistent search state %r", self.search_state)
return
line_index = model.get_path(tree_iter)[0]
forward_search = (self.search_state == "search-forward")
if forward_search:
self.logger.debug("forward search for %r matches line %i",
self.operation.search_text, line_index)
else:
self.logger.debug("backward search for %r matches line %i",
self.operation.search_text, line_index)
self.sentinel.abort()
if self.scroll_match:
self.logger.debug("scrolling to matching line")
self.scroll_view_to_line(line_index)
# Now search for the next one:
self.scroll_match = False
# FIXME: Start with first line that is outside of the visible
# range.
self.start_search_operation(start_position=line_index + 1,
forward=forward_search)
else:
if forward_search:
self.next_match = line_index
self.search_state = "search-backward"
self.start_search_operation(forward=False,
start_position=line_index - 1)
else:
self.prev_match = line_index
self.update_sensitivity()
self.bar.clear_status()
def handle_search_complete(self):
if self.search_state == "search-forward":
self.logger.debug("forward search for %r reached last line",
self.operation.search_text)
self.next_match = None
elif self.search_state == "search-backward":
self.logger.debug("backward search for %r reached first line",
self.operation.search_text)
self.prev_match = None
else:
self.logger.warning("inconsistent search state %r",
self.search_state)
return
self.update_sensitivity()
if self.prev_match is None and self.next_match is None:
self.bar.status_no_match_found()
class Plugin (PluginBase):
features = (FindBarFeature,)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,103 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer Plugins package."""
__all__ = ["_", "_N", "FeatureBase", "PluginBase"]
import os.path
def _N(s):
return s
def load(paths=()):
for path in paths:
for plugin_module in _load_plugins(path):
yield plugin_module.Plugin
def _load_plugins(path):
import imp
import glob
files = glob.glob(os.path.join(path, "*.py"))
for filename in files:
name = os.path.basename(os.path.splitext(filename)[0])
if name == "__init__":
continue
fp, pathname, description = imp.find_module(name, [path])
module = imp.load_module(name, fp, pathname, description)
yield module
class FeatureBase (object):
def __init__(self, app):
pass
def handle_attach_window(self, window):
"""
window: GstDebugViewer.GUI.window.Window
"""
pass
def handle_attach_log_file(self, window, log_file):
"""
window: GstDebugViewer.GUI.window.Window
log_file: GstDebugViewer.Data.LogFile
"""
pass
def handle_detach_log_file(self, window, log_file):
"""
window: GstDebugViewer.GUI.window.Window
log_file: GstDebugViewer.Data.LogFile
"""
pass
def handle_detach_window(self, window):
"""
window: GstDebugViewer.GUI.window.Window
"""
pass
class PluginBase (object):
"""
All plugins must implement a class called Plugin inheriting from PluginBase.
They should place a tuple of features they export into 'features'. Each
feature should be a subclass of FeatureBase.
"""
features = ()
def __init__(self, app):
pass
@@ -0,0 +1,29 @@
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer package."""
version = "@VERSION@"
if version.startswith('@'):
version = 'master'
__version__ = version
from GstDebugViewer.Main import Paths, GETTEXT_DOMAIN, main as run # noqa
@@ -0,0 +1,54 @@
#!/usr/bin/env python
def line_string(ts, pid, thread, level, category, filename, line, function,
object_, message):
# Replicates gstreamer/gst/gstinfo.c:gst_debug_log_default.
# FIXME: Regarding object_, this doesn't fully replicate the formatting!
return "%s %5d 0x%x %s %20s %s:%d:%s:<%s> %s" % (Data.time_args(ts), pid, thread,
level.name.ljust(
5), category,
filename, line, function,
object_, message,)
def main():
import sys
import os.path
sys.path.append(os.path.dirname(os.path.dirname(sys.argv[0])))
global Data
from GstDebugViewer import Data
count = 100000
ts = 0
pid = 12345
thread = int("89abcdef", 16)
level = Data.debug_level_log
category = "GST_DUMMY"
filename = "gstdummyfilename.c"
file_line = 1
function = "gst_dummy_function"
object_ = "dummyobj0"
message = "dummy message with no content"
levels = (Data.debug_level_log,
Data.debug_level_debug,
Data.debug_level_info,)
shift = 0
for i in range(count):
ts = i * 10000
shift += i % (count // 100)
level = levels[(i + shift) % 3]
print(line_string(ts, pid, thread, level, category, filename, file_line,
function, object_, message))
if __name__ == "__main__":
main()
@@ -0,0 +1,79 @@
#!/usr/bin/env python
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer performance test program."""
import sys
import os
import os.path
from glob import glob
import time
import gi
from gi.repository import GObject
from .. import Common, Data, GUI
class TestParsingPerformance (object):
def __init__(self, filename):
self.main_loop = GObject.MainLoop()
self.log_file = Data.LogFile(filename, Common.Data.DefaultDispatcher())
self.log_file.consumers.append(self)
def start(self):
self.log_file.start_loading()
def handle_load_started(self):
self.start_time = time.time()
def handle_load_finished(self):
diff = time.time() - self.start_time
print("line cache built in %0.1f ms" % (diff * 1000.,))
start_time = time.time()
model = GUI.LazyLogModel(self.log_file)
for row in model:
pass
diff = time.time() - start_time
print("model iterated in %0.1f ms" % (diff * 1000.,))
print("overall time spent: %0.1f s" % (time.time() - self.start_time,))
import resource
rusage = resource.getrusage(resource.RUSAGE_SELF)
print("time spent in user mode: %.2f s" % (rusage.ru_utime,))
print("time spent in system mode: %.2f s" % (rusage.ru_stime,))
def main():
if len(sys.argv) > 1:
test = TestParsingPerformance(sys.argv[1])
test.start()
if __name__ == "__main__":
main()
@@ -0,0 +1,281 @@
#!/usr/bin/env python
# -*- coding: utf-8; mode: python; -*-
#
# GStreamer Debug Viewer - View and analyze GStreamer debug log files
#
# Copyright (C) 2007 René Stadler <mail@renestadler.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""GStreamer Debug Viewer test suite for the custom tree models."""
import sys
import os
import os.path
from glob import glob
from unittest import TestCase, main as test_main
from .. import Common, Data
from .. GUI.filters import CategoryFilter, Filter
from .. GUI.models import (FilteredLogModel,
LogModelBase,
SubRange,)
class TestSubRange (TestCase):
def test_len(self):
values = list(range(20))
sr = SubRange(values, 0, 20)
self.assertEqual(len(sr), 20)
sr = SubRange(values, 10, 20)
self.assertEqual(len(sr), 10)
sr = SubRange(values, 0, 10)
self.assertEqual(len(sr), 10)
sr = SubRange(values, 5, 15)
self.assertEqual(len(sr), 10)
def test_iter(self):
values = list(range(20))
sr = SubRange(values, 0, 20)
self.assertEqual(list(sr), values)
sr = SubRange(values, 10, 20)
self.assertEqual(list(sr), list(range(10, 20)))
sr = SubRange(values, 0, 10)
self.assertEqual(list(sr), list(range(0, 10)))
sr = SubRange(values, 5, 15)
self.assertEqual(list(sr), list(range(5, 15)))
class Model (LogModelBase):
def __init__(self):
LogModelBase.__init__(self)
for i in range(20):
self.line_offsets.append(i * 100)
self.line_levels.append(Data.debug_level_debug)
def ensure_cached(self, line_offset):
pid = line_offset // 100
if pid % 2 == 0:
category = b"EVEN"
else:
category = b"ODD"
line_fmt = (b"0:00:00.000000000 %5i 0x0000000 DEBUG "
b"%20s dummy.c:1:dummy: dummy")
line_str = line_fmt % (pid, category,)
log_line = Data.LogLine.parse_full(line_str)
self.line_cache[line_offset] = log_line
def access_offset(self, line_offset):
return ""
class IdentityFilter (Filter):
def __init__(self):
def filter_func(row):
return True
self.filter_func = filter_func
class RandomFilter (Filter):
def __init__(self, seed):
import random
rand = random.Random()
rand.seed(seed)
def filter_func(row):
return rand.choice((True, False,))
self.filter_func = filter_func
class TestDynamicFilter (TestCase):
def test_unset_filter_rerange(self):
full_model = Model()
filtered_model = FilteredLogModel(full_model)
row_list = self.__row_list
self.assertEqual(row_list(full_model), list(range(20)))
self.assertEqual(row_list(filtered_model), list(range(20)))
filtered_model.set_range(5, 16)
self.assertEqual(row_list(filtered_model), list(range(5, 16)))
def test_identity_filter_rerange(self):
full_model = Model()
filtered_model = FilteredLogModel(full_model)
row_list = self.__row_list
self.assertEqual(row_list(full_model), list(range(20)))
self.assertEqual(row_list(filtered_model), list(range(20)))
filtered_model.add_filter(IdentityFilter(),
Common.Data.DefaultDispatcher())
filtered_model.set_range(5, 16)
self.assertEqual(row_list(filtered_model), list(range(5, 16)))
def test_filtered_range_refilter_skip(self):
full_model = Model()
filtered_model = FilteredLogModel(full_model)
row_list = self.__row_list
filtered_model.add_filter(CategoryFilter("EVEN"),
Common.Data.DefaultDispatcher())
self.__dump_model(filtered_model, "filtered")
self.assertEqual(row_list(filtered_model), list(range(1, 20, 2)))
self.assertEqual([filtered_model.line_index_from_super(i)
for i in range(1, 20, 2)],
list(range(10)))
self.assertEqual([filtered_model.line_index_to_super(i)
for i in range(10)],
list(range(1, 20, 2)))
filtered_model.set_range(1, 20)
self.__dump_model(filtered_model, "ranged (1, 20)")
self.__dump_model(filtered_model, "filtered range")
self.assertEqual([filtered_model.line_index_from_super(i)
for i in range(0, 19, 2)],
list(range(10)))
self.assertEqual([filtered_model.line_index_to_super(i)
for i in range(10)],
list(range(1, 20, 2)))
filtered_model.set_range(2, 20)
self.__dump_model(filtered_model, "ranged (2, 20)")
self.assertEqual(row_list(filtered_model), list(range(3, 20, 2)))
def test_filtered_range_refilter(self):
full_model = Model()
filtered_model = FilteredLogModel(full_model)
row_list = self.__row_list
rows = row_list(full_model)
rows_filtered = row_list(filtered_model)
self.__dump_model(full_model, "full model")
self.assertEqual(rows, rows_filtered)
self.assertEqual([filtered_model.line_index_from_super(i)
for i in range(20)],
list(range(20)))
self.assertEqual([filtered_model.line_index_to_super(i)
for i in range(20)],
list(range(20)))
filtered_model.set_range(5, 16)
self.__dump_model(filtered_model, "ranged model (5, 16)")
rows_ranged = row_list(filtered_model)
self.assertEqual(rows_ranged, list(range(5, 16)))
self.__dump_model(filtered_model, "filtered model (nofilter, 5, 15)")
filtered_model.add_filter(CategoryFilter("EVEN"),
Common.Data.DefaultDispatcher())
rows_filtered = row_list(filtered_model)
self.assertEqual(rows_filtered, list(range(5, 16, 2)))
self.__dump_model(filtered_model, "filtered model")
def test_random_filtered_range_refilter(self):
full_model = Model()
filtered_model = FilteredLogModel(full_model)
row_list = self.__row_list
self.assertEqual(row_list(full_model), list(range(20)))
self.assertEqual(row_list(filtered_model), list(range(20)))
filtered_model.add_filter(RandomFilter(538295943),
Common.Data.DefaultDispatcher())
random_rows = row_list(filtered_model)
self.__dump_model(filtered_model)
filtered_model = FilteredLogModel(full_model)
filtered_model.add_filter(RandomFilter(538295943),
Common.Data.DefaultDispatcher())
self.__dump_model(filtered_model, "filtered model")
self.assertEqual(row_list(filtered_model), random_rows)
filtered_model.set_range(1, 10)
self.__dump_model(filtered_model)
self.assertEqual(row_list(filtered_model), [
x for x in range(0, 10) if x in random_rows])
def __row_list(self, model):
return [row[Model.COL_PID] for row in model]
def __dump_model(self, model, comment=None):
# TODO: Provide a command line option to turn this on and off.
return
if not hasattr(model, "super_model"):
# Top model.
print("\t(%s)" % ("|".join([str(i).rjust(2)
for i in self.__row_list(model)]),), end=' ')
else:
top_model = model.super_model
if hasattr(top_model, "super_model"):
top_model = top_model.super_model
top_indices = self.__row_list(top_model)
positions = self.__row_list(model)
output = [" "] * len(top_indices)
for i, position in enumerate(positions):
output[position] = str(i).rjust(2)
print("\t(%s)" % ("|".join(output),), end=' ')
if comment is None:
print()
else:
print(comment)
if __name__ == "__main__":
test_main()