mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-10 03:19:40 +00:00
1196 lines
36 KiB
Python
Executable file
1196 lines
36 KiB
Python
Executable file
#!/usr/bin/python
|
|
# -*- coding: utf-8; mode: python; -*-
|
|
##
|
|
## gst-debug-viewer.py: GStreamer debug log viewer
|
|
##
|
|
## Copyright (C) 2006 Rene 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 2 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 Lesser General Public
|
|
## License along with this library; if not, write to the Free
|
|
## Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
## Boston, MA 02110-1301 USA
|
|
##
|
|
|
|
__author__ = u"René Stadler <mail@renestadler.de>"
|
|
__version__ = "0.1"
|
|
|
|
def _ (s):
|
|
return s
|
|
|
|
import sys
|
|
import os
|
|
import os.path
|
|
from operator import add
|
|
from sets import Set
|
|
import logging
|
|
|
|
import pygtk
|
|
pygtk.require ("2.0")
|
|
|
|
import gobject
|
|
import gtk
|
|
import gtk.glade
|
|
|
|
## import gnome # FIXME
|
|
|
|
import GstDebugViewer.Common.Data
|
|
import GstDebugViewer.Common.GUI
|
|
import GstDebugViewer.Common.Main
|
|
Common = GstDebugViewer.Common
|
|
from GstDebugViewer.Common import utils
|
|
|
|
from GstDebugViewer import Data, Main
|
|
|
|
## # Keep in sync with gst-inspector.py!
|
|
## class MetaModel (gobject.GObjectMeta):
|
|
|
|
## def __init__ (cls, name, bases, dict):
|
|
|
|
## super (MetaModel, cls).__init__ (name, bases, dict)
|
|
|
|
## columns = cls.columns
|
|
## column_types = []
|
|
## def gen ():
|
|
## i = 0
|
|
## it = iter (columns)
|
|
## while True:
|
|
## yield (i, it.next (), it.next (),)
|
|
## i += 1
|
|
## for col_index, col_name, col_type in gen ():
|
|
## setattr (cls, col_name, col_index)
|
|
## column_types.append (col_type)
|
|
|
|
## cls.column_types = tuple (column_types)
|
|
|
|
class LazyLogModel (gtk.GenericTreeModel):
|
|
|
|
__metaclass__ = Common.GUI.MetaModel
|
|
|
|
columns = ("COL_LEVEL", str,
|
|
"COL_PID", int,
|
|
"COL_THREAD", gobject.TYPE_UINT64,
|
|
"COL_TIME", gobject.TYPE_UINT64,
|
|
"COL_CATEGORY", str,
|
|
"COL_FILENAME", str,
|
|
"COL_LINE", int,
|
|
"COL_FUNCTION", str,
|
|
"COL_OBJECT", str,
|
|
"COL_MESSAGE", str,)
|
|
|
|
def __init__ (self, log_obj = None):
|
|
|
|
gtk.GenericTreeModel.__init__ (self)
|
|
|
|
##self.props.leak_references = False
|
|
|
|
self.__log_obj = log_obj
|
|
|
|
self.__line_regex = Data.default_log_line_regex ()
|
|
self.__line_match_order = (self.COL_TIME,
|
|
self.COL_PID,
|
|
self.COL_THREAD,
|
|
self.COL_LEVEL,
|
|
self.COL_CATEGORY,
|
|
self.COL_FILENAME,
|
|
self.COL_LINE,
|
|
self.COL_FUNCTION,
|
|
self.COL_OBJECT,
|
|
self.COL_MESSAGE,)
|
|
self.__line_offsets = []
|
|
self.__line_cache = {}
|
|
|
|
if log_obj:
|
|
self.set_log (log_obj)
|
|
|
|
def set_log (self, log_obj):
|
|
|
|
self.__line_cache.clear ()
|
|
self.__line_offsets = log_obj.line_cache.offsets
|
|
self.__fileobj = log_obj.fileobj
|
|
|
|
def __ensure_cached (self, line_index):
|
|
|
|
if line_index in self.__line_cache:
|
|
return
|
|
|
|
line_offset = self.__line_offsets[line_index]
|
|
|
|
if line_offset == 0:
|
|
self.__fileobj.seek (0)
|
|
line = self.__fileobj.readline ()
|
|
else:
|
|
# Seek a bit further backwards to verify that offset (still) points
|
|
# to the beginning of a line:
|
|
self.__fileobj.seek (line_offset - len (os.linesep))
|
|
line_start = (self.__fileobj.readline () == os.linesep)
|
|
if not line_start:
|
|
# FIXME: We should re-read the file instead!
|
|
raise ValueError ("file changed!")
|
|
line = self.__fileobj.readline ()
|
|
|
|
ts_len = 17
|
|
ts = Data.parse_time (line[:ts_len])
|
|
match = self.__line_regex.match (line[ts_len:-len (os.linesep)])
|
|
if match is None:
|
|
# FIXME?
|
|
groups = [ts, 0, 0, "?", "", "", 0, "", "", line[ts_len:-len (os.linesep)]]
|
|
else:
|
|
groups = [ts] + list (match.groups ())
|
|
|
|
# TODO: Figure out how much string interning can save here and how
|
|
# much run time speed it costs!
|
|
groups[1] = int (groups[1]) # pid
|
|
groups[2] = int (groups[2], 16) # thread pointer
|
|
groups[6] = int (groups[6]) # line
|
|
groups[8] = groups[8] or "" # object (optional)
|
|
|
|
groups = [x[1] for x in sorted (zip (self.__line_match_order,
|
|
groups))]
|
|
self.__line_cache[line_index] = groups
|
|
|
|
def on_get_flags (self):
|
|
|
|
flags = gtk.TREE_MODEL_LIST_ONLY | gtk.TREE_MODEL_ITERS_PERSIST
|
|
|
|
return flags
|
|
|
|
def on_get_n_columns (self):
|
|
|
|
return len (self.column_types)
|
|
|
|
def on_get_column_type (self, index):
|
|
|
|
return self.column_types[index]
|
|
|
|
def on_get_iter (self, path):
|
|
|
|
if len (path) > 1:
|
|
raise ValueError ("flat model")
|
|
|
|
line_index = path[0]
|
|
|
|
return line_index
|
|
|
|
def on_get_path (self, rowref):
|
|
|
|
return (rowref,)
|
|
|
|
def on_get_value (self, rowref, column):
|
|
|
|
if rowref >= len (self.__line_offsets):
|
|
return None
|
|
|
|
self.__ensure_cached (rowref)
|
|
|
|
return self.__line_cache[rowref][column]
|
|
|
|
def on_iter_next (self, rowref):
|
|
|
|
if rowref >= len (self.__line_offsets):
|
|
return None
|
|
else:
|
|
return rowref + 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):
|
|
|
|
return len (self.__line_offsets)
|
|
|
|
def on_iter_nth_child (self, parent, n):
|
|
|
|
if parent or n >= len (self.__line_offsets):
|
|
return None
|
|
else:
|
|
return n ## self.__line_offsets[n]
|
|
|
|
def on_iter_parent (self, child):
|
|
|
|
return None
|
|
|
|
def on_ref_node (self, rowref):
|
|
|
|
pass
|
|
|
|
def on_unref_node (self, rowref):
|
|
|
|
pass
|
|
|
|
class LogModel (gtk.ListStore):
|
|
|
|
__metaclass__ = Common.GUI.MetaModel
|
|
|
|
__gsignals__ = {"loading-progress" : (0, None, (float,),),
|
|
"loading-finished" : (0, None, (),),}
|
|
|
|
columns = ("COL_LEVEL", str,
|
|
"COL_PID", int,
|
|
"COL_THREAD", gobject.TYPE_UINT64,
|
|
"COL_TIME", gobject.TYPE_UINT64,
|
|
"COL_CATEGORY", str,
|
|
"COL_FILENAME", str,
|
|
"COL_LINE", int,
|
|
"COL_FUNCTION", str,
|
|
"COL_OBJECT", str,
|
|
#"COL_MESSAGE_OFFSET", object,)
|
|
"COL_MESSAGE", str,)
|
|
|
|
def __init__ (self):
|
|
|
|
gtk.ListStore.__init__ (self, *self.column_types)
|
|
|
|
self.match_line = default_log_line_regex ()
|
|
self.match_column_order = (self.COL_TIME,
|
|
self.COL_PID,
|
|
self.COL_THREAD,
|
|
self.COL_LEVEL,
|
|
self.COL_CATEGORY,
|
|
self.COL_FILENAME,
|
|
self.COL_LINE,
|
|
self.COL_FUNCTION,
|
|
self.COL_OBJECT,
|
|
#self.COL_MESSAGE_OFFSET,)
|
|
self.COL_MESSAGE,)
|
|
|
|
self.categories = Set ()
|
|
self.objects = {}
|
|
|
|
@staticmethod
|
|
def _filter_func (model, tree_iter):
|
|
|
|
return True
|
|
|
|
def filtered (self):
|
|
|
|
filtered_model = self.filter_new ()
|
|
filtered_model.set_visible_func (self._filter_func)
|
|
return filtered_model
|
|
|
|
def add_parsed (self, args):
|
|
|
|
category = args[4]
|
|
self.categories.add (category)
|
|
|
|
object_name = args[8]
|
|
if object_name:
|
|
self.objects[object_name] = None
|
|
|
|
#args[-1] = args[-1][:60] # FIXME: Message truncated
|
|
|
|
list_iter = self.insert (-1)
|
|
try:
|
|
self.set (list_iter, *(reduce (add,
|
|
zip (self.match_column_order,
|
|
args))))
|
|
except TypeError, exc:
|
|
for column_id, arg in zip (self.match_column_order, args):
|
|
try:
|
|
self.set (list_iter, column_id, arg)
|
|
except TypeError, exc:
|
|
# FIXME
|
|
print >> sys.stderr, str (exc)
|
|
print >> sys.stderr, "column: %i, arg: %r" % (column_id, arg)
|
|
|
|
def parse_line (self, line, offset):
|
|
|
|
if not line.endswith (os.linesep):
|
|
raise ValueError, "line does not end in a line separator"
|
|
|
|
match = self.match_line.match (line[:-1])
|
|
if match is None:
|
|
raise ValueError ("line does not match format")
|
|
groups = list (match.groups ())
|
|
|
|
groups[0] = parse_time (groups[0]) # time
|
|
groups[1] = int (groups[1]) # pid
|
|
groups[2] = int (groups[2], 16) # thread pointer
|
|
groups[6] = int (groups[6]) # line
|
|
groups[8] = groups[8] or "" # object (optional)
|
|
##groups[9] = offset + match.start (10)
|
|
return groups
|
|
|
|
def load_file (self, filename):
|
|
|
|
file_class = file
|
|
if filename.endswith (".gz") or filename.endswith (".gzip"):
|
|
import gzip
|
|
file_class = gzip.GzipFile
|
|
elif filename.endswith (".bz") or filename.endswith (".bz2"):
|
|
import bz2
|
|
file_class = bz2.BZ2File
|
|
|
|
self.fp = file_class (filename, "r")
|
|
self.size = os.path.getsize (filename)
|
|
gobject.idle_add (self.load_deferred ().next)
|
|
|
|
def load_deferred (self):
|
|
|
|
yield True
|
|
|
|
UPDATE = 1000
|
|
|
|
i = 0
|
|
while True:
|
|
offset = self.fp.tell ()
|
|
line = self.fp.readline ()
|
|
if not line:
|
|
break
|
|
|
|
if not line.strip ():
|
|
continue
|
|
|
|
line = strip_escape (line)
|
|
|
|
try:
|
|
self.add_parsed (self.parse_line (line, offset))
|
|
except ValueError, exc:
|
|
print >> sys.stderr, "Cannot parse %s (%s)" % (repr (line), exc)
|
|
|
|
i += 1
|
|
if i % UPDATE == 0:
|
|
self.emit ("loading-progress", min (1.0, float (self.fp.tell ()) / self.size))
|
|
yield True
|
|
|
|
if i % UPDATE != 0:
|
|
self.emit ("loading-progress", 1.0)
|
|
|
|
self.emit ("loading-finished")
|
|
|
|
yield False
|
|
|
|
# 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_sort_func = None
|
|
|
|
def __init__ (self):
|
|
|
|
view_column = gtk.TreeViewColumn (self.label_header)
|
|
view_column.props.reorderable = True
|
|
|
|
self.view_column = view_column
|
|
|
|
# FIXME: Merge with gst-inspector?
|
|
class SizedColumn (Column):
|
|
|
|
default_size = None
|
|
|
|
def compute_default_size (self, view, model):
|
|
|
|
return None
|
|
|
|
# Sync with gst-inspector?
|
|
class TextColumn (SizedColumn):
|
|
|
|
def __init__ (self):
|
|
|
|
Column.__init__ (self)
|
|
|
|
column = self.view_column
|
|
cell = gtk.CellRendererText ()
|
|
column.pack_start (cell)
|
|
|
|
if not self.get_modify_func:
|
|
column.add_attribute (cell, "text", self.id)
|
|
else:
|
|
modify_func = self.get_modify_func ()
|
|
id_ = self.id
|
|
def cell_data_func (column, cell, model, tree_iter):
|
|
cell.props.text = modify_func (model.get (tree_iter, id_)[0])
|
|
column.set_cell_data_func (cell, cell_data_func)
|
|
|
|
column.props.resizable = True
|
|
## column.set_sort_column_id (self.id)
|
|
|
|
def compute_default_size (self, view, model):
|
|
|
|
values = self.get_values_for_size ()
|
|
if not values:
|
|
return SizedColumn.compute_default_size (self, view, model)
|
|
|
|
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)
|
|
max_width = max (max_width, cell.get_size (view, None)[2])
|
|
|
|
return max_width
|
|
|
|
def get_values_for_size (self):
|
|
|
|
return ()
|
|
|
|
class TimeColumn (TextColumn):
|
|
|
|
name = "time"
|
|
label_header = _("Time")
|
|
id = LazyLogModel.COL_TIME
|
|
|
|
@staticmethod
|
|
def get_modify_func ():
|
|
|
|
time_args = Data.time_args
|
|
def format_time (value):
|
|
# TODO: This is hard coded to omit hours as well as the last 3
|
|
# digits at the end, since current gst uses g_get_current_time,
|
|
# which has microsecond precision only.
|
|
return time_args (value)[2:-3]
|
|
|
|
return format_time
|
|
|
|
def get_values_for_size (self):
|
|
|
|
# TODO: Use more than just 0:00:00.000000000 to account for funny fonts
|
|
# maybe? Well, or use monospaced...
|
|
values = [0]
|
|
|
|
return values
|
|
|
|
class LevelColumn (TextColumn):
|
|
|
|
name = "level"
|
|
label_header = _("L")
|
|
id = LazyLogModel.COL_LEVEL
|
|
|
|
@staticmethod
|
|
def get_modify_func ():
|
|
|
|
def format_level (value):
|
|
if value is None:
|
|
# FIXME: Should never be None!
|
|
return ""
|
|
return value[0]
|
|
|
|
return format_level
|
|
|
|
def get_values_for_size (self):
|
|
|
|
values = ["LOG", "DEBUG", "INFO", "WARN", "ERROR"]
|
|
|
|
return values
|
|
|
|
class ThreadColumn (TextColumn):
|
|
|
|
name = "thread"
|
|
label_header = _("Thread")
|
|
id = LazyLogModel.COL_THREAD
|
|
|
|
@staticmethod
|
|
def get_modify_func ():
|
|
|
|
def format_thread (value):
|
|
return "0x%07x" % (value,)
|
|
|
|
return format_thread
|
|
|
|
def get_values_for_size (self):
|
|
|
|
# TODO: Same as for TimeColumn. There is no guarantee that aaaaaaaa is
|
|
# the widest string; use fixed font or come up with something better.
|
|
|
|
return [int ("aaaaaaaaa", 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 FunctionColumn (TextColumn):
|
|
|
|
name = "function"
|
|
label_header = _("Function")
|
|
id = LazyLogModel.COL_FUNCTION
|
|
|
|
def get_values_for_size (self):
|
|
|
|
return ["gst_this_should_be_enough"]
|
|
|
|
## class FullCodeLocation (TextColumn):
|
|
|
|
## name = "code-location"
|
|
## label_header = _("Code Location")
|
|
## id = LazyLogModel.COL_FILENAME
|
|
|
|
## def get_values_for_size (self):
|
|
|
|
## return ["gstwhateverfile.c:1234"]
|
|
|
|
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 = LazyLogModel.COL_MESSAGE
|
|
|
|
# Sync with gst-inspector!
|
|
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.__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.props.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.props.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.props.model
|
|
|
|
self.default_sort = tree_sortable_get_sort_column_id (sort_model)
|
|
|
|
sort_model.set_sort_column_id (TREE_SORTABLE_UNSORTED_COLUMN_ID,
|
|
gtk.SORT_ASCENDING)
|
|
|
|
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.TREE_VIEW_COLUMN_FIXED
|
|
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 not col_class.name 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, ThreadColumn, CategoryColumn,
|
|
FunctionColumn, ObjectColumn, MessageColumn,)
|
|
|
|
def __init__ (self, *a, **kw):
|
|
|
|
ColumnManager.__init__ (self, *a, **kw)
|
|
|
|
self.logger = logging.getLogger ("ui.columns")
|
|
|
|
def attach (self, view):
|
|
|
|
self.view = view
|
|
view.connect ("notify::model", self.__handle_notify_model)
|
|
|
|
ColumnManager.attach (self)
|
|
|
|
def size_column (self, column, view, model):
|
|
|
|
if column.default_size is None:
|
|
default_size = column.compute_default_size (view, model)
|
|
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)
|
|
model = self.view.props.model
|
|
self.size_column (column, self.view, model)
|
|
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):
|
|
|
|
model = self.view.props.model
|
|
for column in self.iter_items ():
|
|
self.size_column (column, view, model)
|
|
|
|
class Window (object):
|
|
|
|
def __init__ (self, app):
|
|
|
|
self.logger = logging.getLogger ("ui.window")
|
|
self.app = app
|
|
|
|
self.sentinels = []
|
|
|
|
self.progress_bar = None
|
|
self.update_progress_id = None
|
|
|
|
self.window_state = Common.GUI.WindowState ()
|
|
self.column_manager = ViewColumnManager ()
|
|
|
|
self.actions = Common.GUI.Actions ()
|
|
|
|
group = gtk.ActionGroup ("MenuActions")
|
|
group.add_actions ([("FileMenuAction", None, _("_File")),
|
|
("ViewMenuAction", None, _("_View")),
|
|
("ViewColumnsMenuAction", None, _("View columns")),
|
|
("HelpMenuAction", None, _("_Help"))])
|
|
self.actions.add_group (group)
|
|
|
|
group = gtk.ActionGroup ("WindowActions")
|
|
group.add_actions ([("new-window", gtk.STOCK_NEW, _("_New Window"), "<Ctrl>N"),
|
|
("open-file", gtk.STOCK_OPEN, _("_Open File"), "<Ctrl>O"),
|
|
("close-window", gtk.STOCK_CLOSE, _("Close _Window"), "<Ctrl>W"),
|
|
("show-about", gtk.STOCK_ABOUT, None)])
|
|
## group.add_toggle_actions ([("show-line-density", None, _("Line _Density"), "<Ctrl>D")])
|
|
self.actions.add_group (group)
|
|
|
|
group = gtk.ActionGroup ("RowActions")
|
|
group.add_actions ([("edit-copy-line", gtk.STOCK_COPY, _("Copy line"), "<Ctrl>C"),
|
|
("edit-copy-message", gtk.STOCK_COPY, _("Copy message"))])
|
|
self.actions.add_group (group)
|
|
|
|
self.actions.add_group (self.column_manager.action_group)
|
|
|
|
self.file = None
|
|
self.log_model = LazyLogModel ()
|
|
## self.log_model.connect ("loading-progress", self.handle_load_progress)
|
|
## self.log_model.connect ("loading-finished", self.handle_load_finished)
|
|
|
|
glade_filename = os.path.join (Main.Paths.data_dir, "gst-debug-viewer.glade")
|
|
self.widget_factory = Common.GUI.WidgetFactory (glade_filename)
|
|
self.widgets = self.widget_factory.make ("main_window")
|
|
|
|
ui_filename = os.path.join (Main.Paths.data_dir,
|
|
"gst-debug-viewer.ui")
|
|
self.ui_factory = Common.GUI.UIFactory (ui_filename, self.actions)
|
|
|
|
self.ui_manager = ui = self.ui_factory.make ()
|
|
menubar = ui.get_widget ("/ui/menubar")
|
|
self.widgets.vbox_main.pack_start (menubar, False, False, 0)
|
|
self.view_popup = ui.get_widget ("/ui/menubar/ViewMenu").get_submenu ()
|
|
|
|
self.gtk_window = self.widgets.main_window
|
|
self.gtk_window.add_accel_group (ui.get_accel_group ())
|
|
self.log_view = self.widgets.log_view
|
|
self.log_view.drag_dest_unset ()
|
|
self.log_view.props.fixed_height_mode = True
|
|
#self.log_view.props.model = self.log_model.filtered ()
|
|
|
|
self.log_view.connect ("button-press-event", self.handle_log_view_button_press_event)
|
|
|
|
self.attach ()
|
|
self.column_manager.attach (self.log_view)
|
|
|
|
## cell = gtk.CellRendererText ()
|
|
## column = gtk.TreeViewColumn ("Level", cell,
|
|
## text = self.log_model.COL_LEVEL)
|
|
## column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
|
|
## column.props.fixed_width = 80 # FIXME
|
|
## self.log_view.append_column (column)
|
|
|
|
## cell = gtk.CellRendererText ()
|
|
## cell.props.family = "monospace"
|
|
## cell.props.family_set = True
|
|
## column = gtk.TreeViewColumn ("Time", cell)
|
|
## #text = self.log_model.COL_TIME)
|
|
## column.set_cell_data_func (cell, self._timestamp_cell_data_func)
|
|
## column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
|
|
## column.props.fixed_width = 180 # FIXME
|
|
## self.log_view.append_column (column)
|
|
|
|
## cell = gtk.CellRendererText ()
|
|
## column = gtk.TreeViewColumn ("Category", cell,
|
|
## text = self.log_model.COL_CATEGORY)
|
|
## column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
|
|
## column.props.fixed_width = 150 # FIXME
|
|
## self.log_view.append_column (column)
|
|
|
|
## cell = gtk.CellRendererText ()
|
|
## column = gtk.TreeViewColumn ("Function", cell,
|
|
## text = self.log_model.COL_FUNCTION)
|
|
## column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
|
|
## column.props.fixed_width = 180 # FIXME
|
|
## self.log_view.append_column (column)
|
|
|
|
## cell = gtk.CellRendererText ()
|
|
## column = gtk.TreeViewColumn ("Object", cell,
|
|
## text = self.log_model.COL_OBJECT)
|
|
## column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
|
|
## column.props.fixed_width = 150 # FIXME
|
|
## self.log_view.append_column (column)
|
|
|
|
## cell = gtk.CellRendererText ()
|
|
## column = gtk.TreeViewColumn ("Message", cell, text = self.log_model.COL_MESSAGE)
|
|
## ##column.set_cell_data_func (cell, self._message_cell_data_func)
|
|
## column.props.sizing = gtk.TREE_VIEW_COLUMN_FIXED
|
|
## self.log_view.append_column (column)
|
|
|
|
def get_top_attach_point (self):
|
|
|
|
return self.widgets.vbox_main
|
|
|
|
def attach (self):
|
|
|
|
self.window_state.attach (window = self.gtk_window, state = self.app.state)
|
|
|
|
self.clipboard = gtk.Clipboard (self.gtk_window.get_display (),
|
|
gtk.gdk.SELECTION_CLIPBOARD)
|
|
|
|
for action_name in ("new-window", "open-file", "close-window",
|
|
"edit-copy-line", "edit-copy-message",
|
|
"show-about",):
|
|
name = action_name.replace ("-", "_")
|
|
action = getattr (self.actions, name)
|
|
handler = getattr (self, "handle_%s_action_activate" % (name,))
|
|
action.connect ("activate", handler)
|
|
|
|
self.gtk_window.connect ("delete-event", self.handle_window_delete_event)
|
|
|
|
self.features = []
|
|
for plugin_feature in self.app.iter_plugin_features ():
|
|
feature = plugin_feature ()
|
|
feature.attach (self)
|
|
self.features.append (feature)
|
|
|
|
def detach (self):
|
|
|
|
self.window_state.detach ()
|
|
|
|
def get_active_line (self):
|
|
|
|
selection = self.log_view.get_selection ()
|
|
model, tree_iter = selection.get_selected ()
|
|
if tree_iter is None:
|
|
raise ValueError ("no line selected")
|
|
return self.log_model.get (tree_iter, *LazyLogModel.column_ids)
|
|
|
|
def close (self, *a, **kw):
|
|
|
|
self.detach ()
|
|
|
|
self.gtk_window.hide ()
|
|
|
|
self.app.close_window (self)
|
|
|
|
def handle_window_delete_event (self, window, event):
|
|
|
|
self.actions.close_window.activate ()
|
|
|
|
def handle_new_window_action_activate (self, action):
|
|
|
|
pass
|
|
|
|
def handle_open_file_action_activate (self, action):
|
|
|
|
dialog = gtk.FileChooserDialog (None, self.gtk_window,
|
|
gtk.FILE_CHOOSER_ACTION_OPEN,
|
|
(gtk.STOCK_CANCEL, 1,
|
|
gtk.STOCK_OPEN, 0,))
|
|
response = dialog.run ()
|
|
dialog.hide ()
|
|
if response == 0:
|
|
self.set_log_file (dialog.get_filename ())
|
|
dialog.destroy ()
|
|
|
|
def handle_close_window_action_activate (self, action):
|
|
|
|
self.close ()
|
|
|
|
def handle_edit_copy_line_action_activate (self, action):
|
|
|
|
self.logger.warning ("FIXME")
|
|
return
|
|
col_id = self.log_model.COL_
|
|
self.clipboard.set_text (self.get_active_line ()[col_id])
|
|
|
|
def handle_edit_copy_message_action_activate (self, action):
|
|
|
|
col_id = self.log_model.COL_MESSAGE
|
|
self.clipboard.set_text (self.get_active_line ()[col_id])
|
|
|
|
def handle_show_about_action_activate (self, action):
|
|
|
|
from GstDebugViewer import version
|
|
|
|
dialog = self.widget_factory.make_one ("about_dialog")
|
|
dialog.props.version = version
|
|
dialog.run ()
|
|
dialog.destroy ()
|
|
|
|
@staticmethod
|
|
def _timestamp_cell_data_func (column, renderer, model, tree_iter):
|
|
|
|
ts = model.get (tree_iter, LogModel.COL_TIME)[0]
|
|
renderer.props.text = Data.time_args (ts)
|
|
|
|
def _message_cell_data_func (self, column, renderer, model, tree_iter):
|
|
|
|
offset = model.get (tree_iter, LogModel.COL_MESSAGE_OFFSET)[0]
|
|
self.log_file.seek (offset)
|
|
renderer.props.text = strip_escape (self.log_file.readline ().strip ())
|
|
|
|
def set_log_file (self, filename):
|
|
|
|
self.logger.debug ("setting log file %r", filename)
|
|
|
|
dispatcher = Common.Data.GSourceDispatcher ()
|
|
self.log_file = Data.LogFile (filename, dispatcher)
|
|
self.log_file.consumers.append (self)
|
|
self.log_file.start_loading ()
|
|
|
|
def handle_log_view_button_press_event (self, view, event):
|
|
|
|
if event.button != 3:
|
|
return False
|
|
|
|
self.view_popup.popup (None, None, None, event.button, event.get_time ())
|
|
return True
|
|
|
|
def handle_load_started (self):
|
|
|
|
self.logger.debug ("load has started")
|
|
|
|
widgets = self.widget_factory.make ("progress_dialog")
|
|
dialog = widgets.progress_dialog
|
|
self.progress_dialog = dialog
|
|
self.progress_bar = widgets.progress_bar
|
|
dialog.set_transient_for (self.gtk_window)
|
|
dialog.show ()
|
|
|
|
self.update_progress_id = gobject.timeout_add (250, self.update_load_progress)
|
|
|
|
def update_load_progress (self):
|
|
|
|
if not self.progress_bar:
|
|
self.logger.debug ("progress window is gone, removing progress update timeout")
|
|
self.update_progress_id = None
|
|
return False
|
|
|
|
progress = self.log_file.get_load_progress ()
|
|
self.logger.debug ("update progress to %i%%", progress * 100)
|
|
self.progress_bar.props.fraction = progress
|
|
|
|
return True
|
|
|
|
def handle_load_finished (self):
|
|
|
|
self.logger.debug ("load has finshed")
|
|
|
|
if self.update_progress_id is not None:
|
|
gobject.source_remove (self.update_progress_id)
|
|
self.update_progress_id = None
|
|
|
|
self.progress_dialog.hide ()
|
|
self.progress_dialog.destroy ()
|
|
self.progress_dialog = None
|
|
self.progress_bar = None
|
|
|
|
## parent_menu = self.glade_tree.get_widget ("view_categories")
|
|
## sub_menu = gtk.Menu ()
|
|
## sub_menu.show ()
|
|
## parent_menu.set_submenu (sub_menu)
|
|
## for category in sorted (model.categories):
|
|
## item = gtk.CheckMenuItem (category, use_underline = False)
|
|
## item.props.active = True
|
|
## item.connect ("toggled", self.handle_view_category_toggled)
|
|
## item.show ()
|
|
## sub_menu.append (item)
|
|
|
|
self.log_model.set_log (self.log_file)
|
|
|
|
for sentinel in self.sentinels:
|
|
sentinel ()
|
|
|
|
def idle_set ():
|
|
self.log_view.props.model = self.log_model #model.filtered ()
|
|
return False
|
|
|
|
gobject.idle_add (idle_set)
|
|
|
|
class AppState (Common.GUI.AppState):
|
|
|
|
geometry = Common.GUI.StateInt4 ("window-geometry")
|
|
maximized = Common.GUI.StateBool ("window-maximized")
|
|
|
|
class App (object):
|
|
|
|
def __init__ (self):
|
|
|
|
self.load_plugins ()
|
|
|
|
self.attach ()
|
|
|
|
def load_plugins (self):
|
|
|
|
from GstDebugViewer import Plugins
|
|
|
|
self.plugins = list (Plugins.load ([os.path.dirname (Plugins.__file__)]))
|
|
|
|
def iter_plugin_features (self):
|
|
|
|
for plugin in self.plugins:
|
|
for feature in plugin.features:
|
|
yield feature
|
|
|
|
def attach (self):
|
|
|
|
state_filename = os.path.join (utils.XDG.CONFIG_HOME, "gst-debug-viewer", "state")
|
|
|
|
self.state = AppState (state_filename)
|
|
|
|
self.windows = [Window (self)]
|
|
|
|
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:
|
|
raise
|
|
else:
|
|
self.detach ()
|
|
|
|
def close_window (self, window):
|
|
|
|
# For some reason, going down takes some time for large files. Let's
|
|
# block until the window is hidden:
|
|
gobject.idle_add (gtk.main_quit)
|
|
gtk.main ()
|
|
|
|
gtk.main_quit ()
|
|
|
|
import time
|
|
|
|
class TestParsingPerformance (object):
|
|
|
|
def __init__ (self, filename):
|
|
|
|
self.main_loop = gobject.MainLoop ()
|
|
self.log_file = Data.LogFile (filename, 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.,)
|
|
|
|
self.start_time = time.time ()
|
|
model = LazyLogModel (self.log_file)
|
|
for row in model:
|
|
pass
|
|
diff = time.time () - self.start_time
|
|
print "data parsed in %0.1f ms" % (diff * 1000.,)
|
|
|
|
def main ():
|
|
|
|
if len (sys.argv) > 1 and sys.argv[1] == "--benchmark":
|
|
test = TestParsingPerformance (sys.argv[2])
|
|
test.start ()
|
|
return
|
|
|
|
app = App ()
|
|
|
|
window = app.windows[0]
|
|
if len (sys.argv) > 1:
|
|
window.set_log_file (sys.argv[-1])
|
|
|
|
app.run ()
|
|
|
|
if __name__ == "__main__":
|
|
main ()
|